C# has no syntax for named properties with parameters, so the expected
output declares the accessors as ordinary methods, keeps the
property-level attributes under the inert 'property:' attribute target
(ignored by csc with CS0657), and consumes the properties through
direct accessor calls, which Roslyn permits exactly for properties
whose shape C# cannot bind. Red against the current decompiler, which
emits a parameterless property whose body references the vanished
parameters.
Assisted-by: Claude:claude-fable-5:Claude Code
The foreach pattern was only matched against using instructions, but
the compiler emits no using/try-finally at all when the enumerator's
static type can never require disposal: a struct or a sealed class
that does not implement IDisposable (SerializationInfoEnumerator in
the issue's example). Such loops stayed while loops.
Recognize the bare 'enumerator = x.GetEnumerator(); while
(enumerator.MoveNext())' shape during statement building and reuse the
existing foreach transformation core for it. The transformation is
restricted to exactly the cases where recompilation would produce the
same IL: the enumerator type rules above (ref structs are excluded
because of pattern-based disposal), a single-store enumerator variable
unused outside the loop, and synchronous enumeration only, since async
enumerators are always IAsyncDisposable.
Assisted-by: Claude:claude-fable-5:Claude Code
Walks the inference algorithm of the standard (draft-v11, 12.6.3) and
adds a test per rule that the revived NRefactory suite did not already
exercise: exact inference for ref parameters and its non-applicability
of the base-type walk, explicit lambda parameter types, exact/upper
bound inference through arrays, nullables and variance nesting, the
unique-base-type restriction, value-type elements forcing exact
inference, conflicting exact bounds, and best common type.
Two rules are pinned as ignored tests because the implementation does
not follow the standard yet: a value argument to an 'in' parameter
infers no bound (12.6.3.7 wants a lower-bound inference), and tuple
literals are not inferred elementwise (12.6.3.7/12.6.3.8). Both tests
assert the csc-verified result and should go green when the rules are
implemented.
Nullable unwrapping in exact and upper-bound inferences needs no
dedicated code path in this implementation: T? is represented as the
constructed type Nullable<T>, so the constructed-type case already
produces the elementwise exact inference the standard asks for; the
new tests pin that equivalence. Function-pointer inference rules and
the explicit-return-type inference of 12.6.3.15 remain untested: the
former needs a MetadataModule to construct FunctionPointerType, the
latter is not representable in LambdaResolveResult.
Assisted-by: Claude:claude-fable-5:Claude Code
The direct unit tests for TypeInference were lost when the NRefactory
sources were replaced by the NuGet package (e88120cb4); since then the
class had no dedicated coverage and ConversionTests still pointed to a
test that no longer existed. Ported to the current type system API and
NUnit constraint asserts. The two tests NRefactory ignored on .NET 4.5
now pin the covariant IReadOnlyList<T> results, since the test
compilation uses the 4.5-era reference mscorlib; the common-subtype list
test gains the ReadOnlyCollectionBuilder<T> candidates contributed by
System.Core, which the NRefactory compilation did not reference.
Also includes the seven tests that only exist in upstream
icsharpcode/NRefactory (async lambdas, NullablePick, CoContraPick,
bug 9300, user-defined-conversion bounds). Upstream wrote them against
its source-based resolver harness, which this repo does not have, so
they are reexpressed as direct InferTypeArguments calls using mock
lambdas and helper types declared in the test assembly. Upstream's
InferFromImplicitAsyncLambda was missing its [Test] attribute and never
actually ran; here it does.
Assisted-by: Claude:claude-fable-5:Claude Code
allows ref struct is inherited implicitly, so restating it on an override is
CS0460 even alongside a legal disambiguator. Roslyn still re-emits the byreflike
flag on the override's own type parameter, and the general constraint printer
turns that flag back into source, so the disambiguator stays legal only as long
as it is built separately. Cover a C# 13 base whose annotated and plain methods
both allow ref structs.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
IsReferenceType is a bool?, so choosing between class, default and no
constraint at all is a three-state decision. Spelling those states out keeps
that visible where the choice is made, rather than leaving it implied by a
comparison against true.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
A class-type constraint such as Stream or Delegate sets no
ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave
those overrides the default constraint, which is CS8822, and the output still
did not recompile. The restated disambiguator leaves no metadata trace of its
own, so the choice has to follow from whether the inherited constraints make
the type parameter a reference type, a value type, or neither.
Matching the annotated type parameters by identity rather than by owner kind
and index also keeps a specialized signature from contributing a foreign type
parameter that happens to share an index.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
Dumping the implicit/explicit classification of all pairs from a 120-type
universe and diffing it against what csc actually compiles surfaced two
divergences (16 affected pairs), both proven by a compiling-and-running
snippet and captured here as ignored known-bug tests:
1. Nullable conversions derived from tuple conversions are missing:
csc accepts "(long, object)? a = t;" for t of type (int, string) as
well as the lifted and explicit forms, CSharpConversions returns None.
The ECMA spec's 10.6.1 does not list tuple conversions as liftable,
so this is a case of Roslyn exceeding the spec.
2. An explicit user-defined conversion to a nullable target is rejected
when the operator result additionally needs an explicit numeric
conversion: csc accepts "(int?)new ImplicitToLong()" (operator to
long, then explicit long -> int?), CSharpConversions returns None.
The same sweep confirmed the SByte..Decimal TypeCode range in
ImplicitEnumerationConversion is correct: csc accepts zero constants of
any numeric type (0.0, 0f, 0m) for enum conversion and rejects '\0',
matching the implementation exactly.
Assisted-by: Claude:claude-fable-5:Claude Code
Audit the conversion test suites against the conversions chapter of the
draft-v8 C# standard and add tests for every rule that had none:
exhaustive implicit/explicit numeric conversion matrix, tuple/ValueTuple
and nullable-annotation identity, interpolated-string / throw-expression /
tuple-literal conversions, the boxing rule set including variance-based
boxing and unboxing, delegate-to-System.Delegate and IReadOnlyList<T>
reference conversions, type-parameter variance and effective-base-class
casts, generic method groups (inference, explicit type arguments, no
inference from the return type), the anonymous-function compatibility
checks CSharpConversions performs itself (via a LambdaResolveResult test
double), standard-conversion exclusion of user-defined operators, and
operators declared in base classes of the source type.
Two rules are implementation gaps rather than test gaps and get ignored
placeholder tests naming the gap: default literal conversions (10.2.16)
are an explicit TODO in CSharpConversions, and switch expressions
(10.2.18) have no ResolveResult representation. The full
section-by-section map is in
Analyses/ILSpy/2026-07-24_conversions-spec-coverage.md.
Assisted-by: Claude:claude-fable-5:Claude Code
Convert the commented-out block in ExplicitConversionsTest the same way as
the implicit ConversionTest block: type-parameter casts via
DefaultTypeParameter with cross-referencing constraints, user-defined
operators as fixture types in the test assembly, constant sources via
ConstantResolveResult. The rr.Input asserts of the originals were resolver
artifacts and are dropped; UseDefinedExplicitConversion_Lifted instead
exercises the ResolveResult-based ExplicitConversion entry point.
Also extend PreferAmbiguousConversionOverReferenceConversion with the
overload-resolution half of the original NRefactory test (the ambiguous
conversion must not prevent M(BB) from being chosen over M(object)), which
the first revival pass had reduced to the conversion classification alone.
Assisted-by: Claude:claude-fable-5:Claude Code
The block of tests inherited from NRefactory's ConversionsTest was
commented out because it depended on ResolverTestBase (full AST +
CSharpResolver). Rewrite all of them against CSharpConversions directly:
hand-built MethodGroupResolveResults over fixture types compiled into the
test assembly (extension methods injected via the internal
extensionMethods field), DefaultTypeParameter instances with
cross-referencing constraints, and metadata-backed fixture interfaces for
the ExpansiveInheritance termination test.
Two deviations from the NRefactory originals:
MethodGroupConversion_RefArgumentObjectVsDynamic now expects a valid
conversion, because object/dynamic mismatch in a ref parameter is an
identity conversion and current csc accepts the assignment (verified);
PreferUserDefinedConversionOverReferenceConversion resolved an invocation,
so it is recast as an OverloadResolution test over FakeMethod candidates.
Assisted-by: Claude:claude-fable-5:Claude Code
Override constraints are normally inherited and omitted, but nullable type
parameters still require class or default to distinguish annotations from
Nullable<T>. Derive that legal discriminator from the method metadata.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
Yield translation derived its target type only from synchronous enumerable
interfaces, leaving async iterator yields untyped and preserving compiler
boxing casts. Use the element type already recovered by the async decompiler.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
ExpressionBuilder printed bitwise & / | on booleans as && / || whenever
the right-hand side was pure, but Roslyn lowers && / || to & / | only
when the right operand is a bare local or parameter read
(LocalRewriter.MakeBinaryOperator, unchanged since 2014). Shapes like
(c == 'a') | (c == 'b') can therefore only originate from a bitwise
source operator, yet were shown as short-circuiting. Per the discussion
in #1545, show the operator the IL actually uses instead of guessing the
source form: the reversal is dropped entirely, so Roslyn-compiled
"a && b" now decompiles to "a & b", which recompiles to the same IL.
Assisted-by: Claude:claude-fable-5:Claude Code
The Avalonia port dropped ILSpy's single-instance feature, leaving three
inert surfaces behind: the --newinstance / --noactivate switches, and the
"Allow multiple instances" option were parsed and persisted but never read,
and every launch started a new process.
The former WPF implementation also encoded the executable location in the
mutex name, so two ILSpy builds at different paths never shared an instance.
That broke the Windows "Open with ILSpy" shell command: it launches a fixed
executable and would not reuse a running instance started from elsewhere.
Reimplement it with portable primitives (named Mutex + named pipes, no
P/Invoke): the first launch for a user takes the mutex and listens; a later
launch forwards its arguments over the pipe and exits. The namespace is
derived from machine + user only -- never the location -- so any launcher
reuses the running instance. --instanceid is a runtime reuse filter matched
against the running instance's actual executable identity (via
Environment.ProcessPath, single-file-bundle safe), not part of the mutex
name, so it never re-introduces the location partitioning it replaces. The
VS add-in passes its bundled exe path as --instanceid to prefer its own
build while still sharing with a plain launch of that same executable.
Assisted-by: Claude:claude-opus-4-8:Claude Code
update-assemblyinfo.ps1 joined the template lines with
Environment.NewLine, so on Linux the generated DecompilerVersionInfo.cs
was written with LF and the pre-commit format hook re-flagged all of its
lines on every single commit. The repo's .cs files are CRLF; write the
generated file that way regardless of platform, and compare with the
same separator so the up-to-date check stays consistent.
Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic index access (a[b]) gave its IndexerExpression a
DynamicInvocationResolveResult with no symbol, so the brackets carried no
tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target
type with the index parameters typed from the callsite delegate, and
attach it. Route it hover-only by detecting a DynamicInvocationResolveResult
directly on the node - which also covers an invoke-member's own
parentheses, so those stop producing a dead navigation link too.
Assisted-by: Claude:claude-fable-5:Claude Code
Lock the hover content the dynamic-tooltip work produces. Ambience-level
cases (CSharpAmbienceTests) pin that SpecialType.Dynamic renders as
"dynamic" and that a synthetic dynamic method renders its return and
per-argument types - including the full hover form, confirming the
unnamed synthetic parameters collapse to their types with no dangling
name. An end-to-end case (HoverOnlyReferenceTests) decompiles a dynamic
call and renders the symbol GetSymbol hands back, exercising the actual
synthesis (argument typing from the callsite delegate), not a hand-built
stand-in.
Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic invocation with explicit type arguments (a.Method<int>())
synthesized a non-generic fake method, so the hover showed Method(...)
without the generic list. Give the fake a matching set of conventionally
named type parameters (T, T2, ...) and specialize it with the actual
arguments, as a real generic call produces a SpecializedMethod, so the
hover shows the method's generic parameters.
Assisted-by: Claude:claude-fable-5:Claude Code
References had two behaviors encoded in one bool: navigable links, and
"local" references that are non-navigable but highlight all occurrences
on click. Synthesized dynamic members were routed through the latter, so
they picked up the occurrence highlight even though each use is a
distinct synthetic member with nothing to group.
Model the three modes explicitly with a ReferenceMode enum on
ReferenceSegment (Link / LocalHighlight / HoverOnly). Dynamic members,
the dynamic constructor and the dynamic keyword now use HoverOnly: they
still show a hover tooltip (BuildHoverContent resolves any IEntity
reference regardless of mode) but get the arrow cursor, do not navigate,
and do not highlight. Local variables keep LocalHighlight. Plumbed via a
new isHoverOnly flag on ITextOutput.WriteLocalReference.
Assisted-by: Claude:claude-fable-5:Claude Code
A dynamic object creation (new T(b) where an argument is dynamic) gave
its ObjectCreateExpression only a plain ResolveResult(T), so neither the
type name nor the parentheses referenced a constructor - unlike a normal
new expression, whose CSharpInvocationResolveResult lets both hover the
ctor. Synthesize a constructor on the created type (parameters typed from
the callsite delegate) and attach it the same way. Since it has no
metadata, route it hover-only, like the other dynamic members, so the
type name's existing navigation is not replaced by a dead link.
Assisted-by: Claude:claude-fable-5:Claude Code
The dynamic keyword fell through WritePrimitiveType's default arm, so it
was emitted as plain text with no reference segment and thus could not
carry a tooltip - unlike int/string/object, which reference their
metadata type. Emit dynamic as a hover-only reference to
SpecialType.Dynamic (no navigation target, since it has no metadata
definition) and render it in BuildHoverContent.
Assisted-by: Claude:claude-fable-5:Claude Code
Local variable references are emitted as reference segments (via
WriteLocalReference), but their reference object is an ILVariable, which
BuildHoverContent could not resolve to an IEntity - so hovering a local
showed nothing. Render the declared type (syntax-highlighted, via a new
Language.GetRichText(IType) overload) with the name and whether it is a
parameter or a local, directly from the ILVariable.
Assisted-by: Claude:claude-fable-5:Claude Code
A member synthesized for a dynamic access has no metadata token, so the
navigation link it produced went nowhere. Emit it as a local-style
reference (WriteLocalReference) instead: the hover renderer still shows
the signature (it resolves any IEntity reference regardless of IsLocal),
but the identifier is no longer a navigation target, matching how local
variables are treated. The written text is unchanged.
Assisted-by: Claude:claude-fable-5:Claude Code
Dynamic member accesses and invocations carried only the member name
(DynamicMemberResolveResult / DynamicInvocationResolveResult), so
GetSymbol returned null and the editor emitted no reference or hover.
Synthesize a member on the target type - a dynamic field for a member
access, a dynamic-returning method for a member invocation - named after
the accessed member and typed from the callsite delegate: each argument
uses its recorded compile-time type when the binder set one (statically
typed or constant arguments), dynamic otherwise, and the declaring type
comes from the receiver's argument info. Route these through GetSymbol;
TextTokenWriter and the hover renderer already turn an IEntity into a
tooltip. The synthesized members have no metadata token, so they render
a signature on hover but are not navigation targets.
Assisted-by: Claude:claude-fable-5:Claude Code
Obfuscators may rename the members of compiler-generated iterator
classes while keeping the MethodImpl (.override) rows intact. Since
b110d5c2d (first shipped in ILSpy 8.0), Dispose, get_Current and
GetEnumerator are identified by name or method impl; MoveNext was still
looked up by name only, so such a state machine was rejected with
"Method not found". The pre-Roslyn "yield break" handling likewise
matched the Dispose call by name; it now compares against the already
resolved Dispose handle. The new ILPretty fixture models the obfuscated
shape from the issue's assemblies, against which the fix was verified;
the rest of the issue was already resolved by b110d5c2d and 81e702f84.
Assisted-by: Claude:claude-fable-5:Claude Code
TypeSystemInitStop now reports the size of the final reference set passed
to Init() instead of the raw resolve count, which included same-name
duplicates that the version dedup later drops. The schema smoke tests
asserted exact global event counts, but the providers are process-wide
and the decompiler fixtures run in parallel, so unrelated decompilations
could inflate the counts; every string payload now carries a unique
marker (and the string-less snapshot events a sentinel count) that the
assertions filter on.
Assisted-by: Claude:claude-fable-5:Claude Code
Collecting from the two EventSource providers differs by tooling, not by
platform: dotnet-trace/EventPipe works the same everywhere, with ETW/PerfView
as the Windows-only alternative. The doc lists the keyword masks and levels
so sessions can enable only the areas of interest.
Assisted-by: Claude:claude-fable-5:Claude Code
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
The events introduced in #2519 timed only the five per-entity DoDecompile
overloads, allocated a Stopwatch and the member's FullName even when no
trace session was attached, and reported whole milliseconds, which rounds
almost every member to zero. Flat one-shot events also gave PerfView and
dotnet-trace no way to show durations or nesting, and the actually
expensive stages (type system initialization, assembly resolution probing,
the IL/AST transform pipelines, whole-project decompilation) were not
instrumented at all.
Start/Stop event pairs let trace viewers derive duration and nesting from
event timestamps, keywords let a session enable only the areas of
interest, and every call site is gated on IsEnabled() so tracing costs a
branch when disabled. Per-transform events are Verbose because of their
volume; unlike the STEP/Stepper mechanism they work in Release builds.
EventSource is in-box for netstandard2.0 and flows over both ETW and
EventPipe, so this stays cross-platform with no new dependency.
Assisted-by: Claude:claude-fable-5:Claude Code
Enum members whose value duplicates an earlier member now reference it
(Item2B = Item2A), [Flags] members combine earlier single-bit members
(All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1)
instead of showing a bare number.
Several guardrails keep the output faithful to how such enums are
written by hand: only previously declared members are referenced (field
row order); a multi-bit value lying entirely within a larger, earlier
member is a field encoding inside that mask, not a flag union, and
stays numeric, as do zero-valued members of [Flags] enums, which
routinely have several unrelated zero members. The ~X form is
suppressed in byte/ushort enum declarations, where the initializer
constant folds in int and would not compile. Enums with unusual
underlying types (bool, native int) keep the plain constant conversion.
With these rules, decompiling System.Private.CoreLib reproduces the
hand-written declarations of TypeAttributes, MethodAttributes,
AttributeTargets and FileAttributes almost verbatim.
Assisted-by: Claude:claude-fable-5:Claude Code
ReduceNesting walks an else-if chain to its innermost if and asks
ShouldReduceNesting whether to extract the else block, which ExtractElseBlock
does by casting the block to Block. A chain with no trailing else reaches this
with a bare Nop, yet the heuristic still approved it (its stats count a Nop as
one statement), so the cast threw InvalidCastException. Take a Block in
ShouldReduceNesting and skip the reduction at the call site when the else is
absent.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.
mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
MemberIsHidden decides field hiding from the metadata name association
alone, which over-approximates: an event whose accessors fail the ILAst
validation is decompiled with explicit accessors, and while a referenced
backing field is re-added through the work list, an unreferenced one was
silently dropped from the output. The type-definition member loop now
consults the same memoized verdict as the event declaration, so the two
decisions agree by construction.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).
Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.
mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.
Assisted-by: Claude:claude-fable-5:Claude Code
Structural recognition of compiler-generated code needs method bodies as
ILAst decompiled with a fixed set of settings, so that recognition does
not depend on user-visible options. RecordDecompiler had this pipeline
as a private helper; hoist it to CSharpDecompiler so other recognizers
can share it, deriving the generic context from the method's declaring
type instead of a captured type definition.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
In nested-namespace mode a NamespaceTreeNode's display label is only its
last segment ("Generic"), while the full dotted path
("System.Collections.Generic") is what identifies the namespace in
metadata and to the docs site. Three call sites read the label where they
need the full path, so in nested mode each targets the wrong namespace:
decompiling a namespace node queries an empty one and titles the output
after the last segment; the MSDN URL points at the wrong page; and
scope-search-to-namespace scopes to the wrong name.
These are ds5678's fixes from #3879, reintegrated on top of the eager
namespace rebuild. #3879's other Name -> FullName corrections, in
AssemblyTreeNode.FindNamespaceNode and TreeNodeLocator, are already
covered here by the full-namespace-name and type-handle indexes, so only
the Decompile and search-entry cases carry over.
Assisted-by: Claude:claude-opus-4-8:Claude Code
With "Use nested namespace structure" enabled, most namespaces never
appeared in the tree, and the first expand of a large assembly lagged.
Both come from the same regression: the Avalonia assembly-tree nodes
were written from scratch as lazy scaffolding, not ported from the WPF
design, and lost the single eager build the WPF host used.
A NamespaceTreeNode filters as Recurse/MatchAndRecurse, so the filter
cascade computes its IsHidden as "all children hidden" -- vacuously true
for an empty child set. The lazy build attached each namespace node
while it was still empty, latching intermediate namespaces (those that
hold only sub-namespaces, e.g. System.Collections) hidden and stranding
everything beneath them. The cascade also force-loads every namespace
node's children anyway, so the per-node laziness avoided no work: it
rescanned the whole TypeDefinitions table once per namespace node.
Restore release/10.1's structure: AssemblyTreeNode builds the entire
namespace band in one pass over the module's top-level types, populates
each node before attaching it, and keeps two indexes -- full namespace
name -> node and type handle -> node -- so FindNamespaceNode/FindTypeNode
are O(1) and correct at any nesting depth. TreeNodeLocator.FindTypeNode
(hyperlink clicks, search activation, JumpToType) delegates to that
index instead of walking children by display name, which never matched
in nested mode. NamespaceTreeNode goes back to a dumb label holder and
re-escapes its display label via ILAmbience.EscapeName.
The band is built from the module's type system, like 10.1's, not from
raw metadata: each TypeTreeNode holds the resolved ITypeDefinition it
renders from, so painting a cell no longer re-enters the settings-keyed
type-system cache the way master's lazy node did on every Text/Icon/
Filter read -- each of which rebuilt an effective-settings object and
took its lock. Ordering the pass by full ReflectionName is also what
interleaves a namespace's types and its sub-namespaces into one
alphabetical run (a sub-namespace attaches when its first descendant
type is reached, landing at its own alphabetical slot among the sibling
types); grouping all types ahead of all namespaces was a visible
departure from the WPF order.
Two deliberate departures from a literal 10.1 copy: keep the global-
namespace "-" node, and keep the cached IsPublicAPI getter. Both index
dictionaries are cleared on rebuild so a nested/flat toggle leaves no
stale entries.
Holding resolved entities means the tree has to be rebuilt when a
setting changes the type system. Only one compilation is cached per
module, keyed on the effective decompiler settings, so a language-
version or decompiler-option change drops it and would otherwise leave
every node pointing at a discarded compilation -- stale labels, icons
and filters, and the C# 14 extension-block nodes shown against the wrong
version. AssemblyTreeModel reloads the loaded assemblies when the
computed TypeSystemOptions actually change (Display-only settings never
do, and cost nothing), then restores the selected node from its path --
which re-expands its ancestors on the way to revealing it -- the way
Refresh does. The WPF host got this for free: its modal Options dialog
rebuilt the tree on close, where the Avalonia page applies live.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Solution export reported once per assembly, when that assembly finished.
Nothing was reported before the first one did, so the tab sat on the
indeterminate spinner it starts with for most of the run and then jumped
straight to the end -- exporting two assemblies showed a spinner, 1 of 2,
done. A project that bailed out before decompiling never reported at all,
stranding the bar short of the end for the rest of the export.
Sum the per-project file counts instead: each parallel worker feeds its own
counts into a shared map and the bar reports their total. WholeProjectDecompiler
carries its whole file count on every report, so the total is known from a
project's first written file rather than its last -- measured on two real
assemblies, the bar turns determinate after 245ms instead of 15s, and moves
through 978 files rather than 2 assemblies. Each project closes its share out
in a finally, so bailing out or cancelling still lets the bar reach the end.
The denominator grows over the first second as projects discover their file
counts. The alternative -- enumerating every project's types up front -- delays
the export itself to make the bar look better, which is the wrong trade.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two gaps in the export paths, both visible from the same selection.
A selection holding an assembly that failed to load was turned away by
TryGetExportableAssemblies, so Ctrl+S fell through to the single-node save
and quietly wrote just the focused assembly -- the rest of the selection
vanished with no report. The predicate now only insists that something in
the selection loaded, and the exporter skips what it cannot decompile and
names it in the status report. That is also what the dialog always assumed:
its "not a valid assembly" row badge was unreachable, because no selection
containing one could get that far.
The dialog asks for an output folder and derived the .sln name from it,
while Save Code lets the user name the file. Now the dialog offers the name
too, in solution mode, defaulting (via the placeholder) to the folder-
derived name the exporter would pick anyway.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Save Code on several assemblies had its own copy of the export flow: its
own selection matcher, its own frozen-tab runner, and a hard-coded
"Exporting solution" tab title -- so the same operation read differently
depending on whether it was started from Save Code or Export Project,
which titles the tab after the assemblies. Route it through ProjectExport
like the single-assembly path already is, leaving one runner and one
matcher behind every flow that decompiles whole assemblies to disk.
Save Code keeps letting the user name the .sln (the Export Project dialog
only asks for a folder and derives the name from it), so the export
options now carry an optional solution file name; unset means the old
folder-derived name.
Assisted-by: Claude:claude-opus-4-8:Claude Code
File -> Save Code decompiled a whole assembly on a bare Task.Run: no
progress bar, no way to cancel, and (for a .csproj) diverging from both
normal decompilation and the dedicated Export Project command, which
already report progress. Route the assembly-save paths through the shared
UI instead:
- The .csproj export reuses the Export Project machinery (ProjectExporter
in a frozen, determinate-progress tab), so a large assembly reports
per-file progress and can be cancelled while the tree stays browsable.
- The single-file save runs behind the same RunWithCancellation overlay
that normal decompilation uses.
Both entry points into the project export compute the tab title in one
place, titling it after the assemblies being exported (their tree-node
labels, joined the way a multi-node decompile tab is) so the tab reads
the same whether reached via Save Code or Export Project.
Assisted-by: Claude:claude-opus-4-8:Claude Code