The analyzer tree set ShowLines="False" while the assembly tree uses "True", so
the Analyze panel was missing the connector lines the rest of the app shows. The
two panes already share the same SharpTreeView control and templates, and the
line geometry is driven entirely by the generic node Level/IsLast state, so the
analyzer hierarchy renders them correctly; the False was an unconsidered default
carried in when the pane was migrated to SharpTreeView. Align it with "True".
Assisted-by: Claude:claude-opus-4-8:Claude Code
The analyzer signature colours depend on the active theme, and the formatting
on the active language, but the highlighted RichText was built once and cached.
Toggling the theme or switching the decompiler language therefore left existing
analyzer results showing the old colours / syntax.
Key the node's RichText cache on the (theme, language) pair so it rebuilds when
either changes, and have the rich-text cell re-render rich rows on both
ThemeManager.ThemeChanged and the language settings' PropertyChanged (cleaned up
on Node change and on detach, like the existing subscriptions).
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Analyze panel showed each signature as a flat string, so the type names
that carry the key information were hard to spot in long Used-By / Uses lists.
Render analyzer entity rows as syntax-highlighted rich text with the type-name
spans emboldened (issue #2164).
Replace Language.GetRichTextTooltip(entity) with a general
GetRichText(entity, conversionFlags, boldTypeNames): the hover tooltip is one
caller, the analyzer pane another. C# colours the signature via
CSharpHighlightingTokenWriter and bolds the type-name colour spans; IL renders
its disassembled header (its signature form), already lexically highlighted;
the base produces plain text. The IL method/type/field header is semantically
the same artifact as the C# signature, so it shares the one method.
Rendering is opt-in: nodes that want rich labels implement IRichTextNode, and
the single shared SharpTreeView cell renders its runs via the new RichNodeText
attached behaviour, falling back to the plain Text otherwise. SharpTreeNode.Text
stays the authoritative plain string, so search, copy and keyboard navigation
are unchanged; analyzer entity nodes derive Text from the same RichText so the
two never diverge.
Fixes#2164
Assisted-by: Claude:claude-opus-4-8:Claude Code
The right-click menu in the decompiled code read in a near-random order. The
shared entry registry orders entries by a single Order value and only uses
Category to place separators, so a few defects scrambled the result: the
"Decompile" reference entry had Order=10, jumping above Copy/Select all; the
"Decompile to new panel" entry sat in a misspelt "Navigate" category (vs the
"Navigation" the other entries use), adding a stray separator; and it tied
Copy at Order 100 while "Go to token" tied Analyze at 200, interleaving
categories on the discovery-order tiebreak.
Pull the three navigation entries into a contiguous 150-170 band so they form
one block between the Editor (100s) and Analyze (200s) bands, fixing the
spelling so they share a category. No provider change: the existing by-hundreds
category layout is left intact, so the tree / search / metadata menus that share
the registry stay coherent.
Assisted-by: Claude:claude-opus-4-8:Claude Code
F# emits tail calls pervasively, but the 'tail.' prefix was dropped
entirely at the C# output stage, so the information never reached the
decompiled text. Render it inline as '/*tail.*/' before the call,
mirroring the existing 'constrained.' prefix comment in CallBuilder.
Fix#3817
Assisted-by: Claude:claude-opus-4-8:Claude Code
The Simple theme sizes a menu row to its 16px icon plus margins (~24px), tighter
than the classic WPF menu. Raise MinHeight so the rows have comparable vertical
breathing room, and centre the icon, label and input-gesture text in the taller
row (the theme template leaves their VerticalAlignment unset, so a style reaches
them) instead of letting them sit at the top.
Assisted-by: Claude:claude-opus-4-8:Claude Code
* Migrate ILSpy.AddIn.VS2022 to SDK-style VSIX project
* Retire the legacy VS2017/2019 ILSpy add-in
* Build the VS extension via dotnet build on the slnx
* Inline ILSpy.AddIn.Shared into ILSpy.AddIn.VS2022
* Restore the VS add-in menus broken by the SDK-style migration
The "Open from NuGet feed" chooser rendered every result with the default
NuGet logo, even though the feed search already returns each package's own
IconUrl - it was fetched into NuGetPackageInfo and then ignored. The list
looked like a wall of identical icons, unlike the NuGet gallery.
NuGetPackageInfo is an immutable feed DTO bound straight into the ListBox, so
it can't raise PropertyChanged for an icon that arrives later. Rather than
bolt mutability and an Avalonia image type onto that DTO, introduce a thin
per-row view model (NuGetPackageViewModel) that owns an observable Icon
defaulting to the logo and swaps in the real icon once downloaded, plus an
INuGetIconLoader service that fetches and decodes off the UI thread. Icons
are decoded to 64px (rendered at 32) and cached by URL as the in-flight Task
so each URL downloads once and concurrent requests share it; the loader is
held for the command's lifetime so the cache survives reopening the dialog.
A per-search CancellationTokenSource stops a stale result set's downloads
from painting onto rows recycled by the next search. Any failure - non-http
URL, network error, decode error - collapses to null so a dead icon URL just
keeps the default.
Also disable horizontal scrolling on the results list. With it enabled
(the default), rows measured at unbounded width, so the star column stopped
filling the viewport and selecting a row brought the overflow into view -
the list jumped sideways and grew a scrollbar. The row layout is meant to
fit the width (the description wraps/trims), so horizontal scrolling was
never wanted.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The project listed 201 explicit <Compile Include> and 119 <None Include> items
with EnableDefaultItems=false. Many test-case sources had been marked <None>
only because the C# compiler available when they were written could not build
them; the current Roslyn compiles them fine. The hand-maintained lists had also
drifted: five committed fixtures (Operators.cs, Issue3751.cs, three ILPretty
.cs) were on disk with passing tests but in no item list.
Switch to the SDK default **/*.cs glob and exclude only the sources that still
cannot be compiled into the test assembly, determined empirically by building
with everything included and removing what failed:
- IL-pretty inputs that are not valid standalone C# and *.Expected.cs golden
outputs that reuse type names (compile errors / duplicate definitions).
- MetadataAttributes.cs, which applies assembly/module attributes that break
NUnit test discovery (zero tests found) when compiled into the assembly.
The excluded sources stay <None> for IDE visibility; the harness compiles them
from disk at test time regardless. This compiles 23 more fixtures than before
while keeping every previously-compiled file. Default None globbing stays off so
the non-C# inputs (.il/.vb/.fs) remain the authoritative list.
Operators.cs, now part of the build, is normalized to the repo's tab indentation
by the format hook (a CS110 block used spaces). Verified: the full suite still
reports 2257 passed, 0 failed, 35 skipped.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The project listed all 576 source files as explicit <Compile Include> with
EnableDefaultItems=false, so every new file had to be added by hand. Switch to
the SDK's default **/*.cs glob and keep only the exclusions that are actually
needed:
- Properties/DecompilerVersionInfo.cs is generated before build and absent when
the glob is evaluated on a clean checkout, so it is still included explicitly
(with a paired Remove to avoid a duplicate once a prior build produced it).
- DecompilerVersionInfo.template.cs is a placeholder template, never compiled.
Also delete the hand-written DecompilerAstNodeAttribute.cs: the source generator
emits this attribute (RegisterPostInitializationOutput), so the on-disk copy was
stale dead code -- never compiled, and parameterless where real usages pass
[DecompilerAstNode(hasPatternPlaceholder: true)]. Removing it drops what would
otherwise be a third exclusion.
Default None globbing stays off so the explicit None entries remain the
authoritative list. The evaluated Compile set is identical to the previous 576
files; verified by a clean build with the version file both present and absent.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A stackalloc whose result is a pointer is only valid C# as the initializer of a
pointer-typed local. The inliner moved a single-use pointer stackalloc into its
use, producing e.g. 'K.V(stackalloc int[3] { 1, 2, v })' or '*stackalloc ...';
in an expression position the stackalloc is typed as Span<T>, which does not
convert to a pointer, so the output did not compile. Keep such a stackalloc as a
separate local. Moving it into a local store (its declaration) and into the
Span<T>/ReadOnlySpan<T> constructor stay allowed, since those are the positions
where the pointer or span form is exactly what is wanted.
Found while exploring stackalloc-initializer coverage.
Assisted-by: Claude:claude-opus-4-8:Claude Code
HandleSequentialLocAllocInitializer formed a stackalloc initializer whenever the
explicit stores were contiguous from offset 0, even if they did not cover the
whole buffer. A 'stackalloc byte[16]' reinterpreted as int and written through
three of its four elements decompiled to 'stackalloc int[4] { 1, v, 3 }', whose
initializer has fewer elements than the declared length and does not compile.
Require every element to be written (from the constant data blob or an explicit
store) before forming the initializer; otherwise the buffer stays a plain
stackalloc with individual stores.
Found while exploring stackalloc-initializer coverage.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A constant-size stackalloc initializer stores its constant elements through a
data blob (cpblk from a <PrivateImplementationDetails> field). ReadElement
decoded every element by width, so a 4-byte float was read as Int32 and an
8-byte double as Int64. The resulting constant carried the raw bit pattern
(1f decoded as 1.0653532E+09f) and its stack type no longer matched the store,
tripping StObj.CheckInvariant. Dispatch on the element's type code so Single and
Double are read as floating-point, matching the heap-array decoder.
Found while exploring stackalloc-initializer coverage around the element-type
hint fix; floating-point element types had no test case.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A constant-size stackalloc initializer whose buffer is only passed to a static
call (never dereferenced as a typed pointer) threw "given Block is invalid!".
TranslateStackAllocInitializer recovers the element type from the surrounding
type hint, but that hint is unreliable for this shape: the constant allocation
size is folded to a byte count, so it can no longer be read off a
'count * sizeof(T)' expression, and the buffer is kept on the stack as a native
int instead of a T* local, leaving the hint a non-pointer. The guard only
repaired an incompatible pointer hint, so a non-pointer hint fell through to a
'byte' element type that is incompatible with the actual stores, and the
per-element check threw. Derive the element type from the type being stored
whenever the hint is not already a compatible pointer.
The regression is driven by the code shape, not the compiler version: in
optimized builds the buffer is kept on the stack as a native int whenever it is
passed to a static call without being dereferenced. This reproduces across
Roslyn versions, including the pinned test compiler, so the added fixture is red
without this fix in the optimized configurations.
Assisted-by: Claude:claude-opus-4-8:Claude Code
After the Role and TokenRole hierarchies were removed, the Roles class no
longer held any roles -- only the punctuation and keyword text the output
visitor writes (LPar, Arrow, ClassKeyword, ...). The name was a misleading
vestige of the deleted system. Rename it to Tokens, which is what it now
is; the constants and their values are unchanged.
Also refresh the CSharpSlotInfo doc, which still described slot identity as
the successor to the removed node.Role == Roles.X comparison.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A slot kind names one child position, so the generator emits a single
typed Slots.X constant for it. A kind used with two different child types
had to widen that constant to AstNode, and the typed GetChildren<T>
accessor then cast the live AstNodeCollection<Concrete> to
AstNodeCollection<AstNode> -- an InvalidCastException waiting for the
first caller (latent today, but the model permitted it).
Make the loose state unrepresentable instead of guarding it at runtime:
DSTG001 errors when a [Slot] kind is declared with more than one child
type. The six kinds that only coincidentally shared a name (Body, True,
False, Initializer, SwitchSection, Variable) are split into precisely
typed kinds; a genuinely either/or position (a lambda body) stays one
declared type, AstNode. Every Slots.X now carries its real element type,
so the GetChildren cast can no longer fail.
Output is unchanged: the Pretty suite stays byte-identical with
CheckInvariant green in DEBUG.
Assisted-by: Claude:claude-opus-4-8:Claude Code
These helpers are documented to return null when nothing matches, but
returned `null!`, hiding the very nullable warnings #nullable enable is
meant to surface: a miss handed back null typed as non-null and would
NRE downstream with no compile-time signal. Returning T? lets the
compiler enforce the guard at each call site (both existing callers
already null-check). GetVariable forwards the result, so it becomes
VariableInitializer? to match.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A single slot always occupies the same flattened index, so filling,
clearing, or replacing it moves no other child -- only the new child's own
index needs setting. The generated single-slot setters now pass that index
to SetChildNode (a compile-time constant when no collection precedes the
slot; SetChild forwards its argument), which assigns childIndex directly and
never invalidates -- mirroring the IL AST's SetChildInstruction(ref, value,
index). Previously a set-from-null could not know the index here and fell
back to invalidating, forcing a later O(children) EnsureChildIndices rebuild.
With the indices now kept current by construction, NextSibling/PrevSibling
inline the validity check and skip the (non-inlinable) EnsureChildIndices
call in the overwhelmingly common already-valid case.
Together these idle the renumber machinery on a System.Private.CoreLib
decompile: EnsureChildIndices calls 53.4M -> 1.6M, actual rebuilds
1.97M -> 183K, elements renumbered 3.40M -> 757K. Output is byte-identical
and the Pretty suite stays green with CheckInvariant validating the
directly-assigned indices after every transform.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Removing a collection element invalidated the parent's whole flattened
index set, so the next sibling navigation rebuilt it in O(children) -- and
that renumber, not the array shift, was the dominant cost: a reverse
(tail-first) removal, which shifts nothing, was still quadratic, isolating
EnsureChildIndices as the culprit.
When a node's only collection is also its last slot it owns the contiguous
range [base, base + Count) with nothing after it, so an element's flattened
index is just base + its local position (base is the slot's declaration
position, since the preceding slots are all single children). On that
fast-path -- which the generator now flags, passing the base index -- Add
indexes only the appended element, Insert/Remove renumber only the shifted
suffix, and IndexOf is base-relative O(1); none of them invalidate. Other
shapes (several collections, or a slot after the collection) keep the
invalidate-and-rebuild fallback. Tail and scattered removal, and removal
during traversal, no longer pay the per-operation renumber.
Microbenchmark, removing every element of an N-element block: tail-first at
N=32000 went 1493 ms -> 0.5 ms (now O(N)); front-first is ~1.6x faster and
no longer renumbers (its residual cost is the inherent array shift). Output
is byte-identical and the Pretty suite stays green with CheckInvariant
validating the maintained indices after every transform.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A node's children carry a cached flattened childIndex, rebuilt by
EnsureChildIndices after any structural mutation invalidates it. The
single-slot setter and the collection indexer invalidated the whole set on
every in-place replace, so the next sibling navigation (e.g. the visitor's
NextSibling walk) rebuilt all indices in O(children). A transform that
replaces each element of a block while traversing it was therefore O(N^2).
But replacing a child in place does not move anything: a single slot always
occupies the same flattened index, and a replaced collection element keeps
its position. So carry the old child's index to the new child and skip the
invalidation. Setting or clearing a slot still invalidates, since the new
child's index is not known locally; a stale carried value is corrected by
the next renumber anyway.
Microbenchmark (replace every statement in an N-statement block): at
N=32000, 2158 ms -> 1 ms. Output is byte-identical and the Pretty suite
stays green with CheckInvariant active.
Assisted-by: Claude:claude-opus-4-8:Claude Code
AstNodeCollection<T> created its List<T> eagerly in the constructor, and
the collection itself is created on first access of its slot property. So
every collection slot that is read but stays empty -- Attributes,
TypeArguments, type-parameter constraints and the like, which are absent on
the vast majority of nodes -- still allocated a List that never held an
element. Decompiling System.Private.CoreLib that was ~100 MB of short-lived
empty Lists churning gen0.
Make the list nullable and allocate it on first Add; an accessed-but-empty
collection now costs only the wrapper. The backing array was already lazy
(List defers it to the first add), so this drops the redundant List object
for the empty case. Output is byte-identical and the Pretty suite stays
green with CheckInvariant active.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The generated GetChildNodes materialized a List<AstNode> (plus a boxed
List.Enumerator at every foreach) for each node, so AstNode.Children and
the visitor's per-node child walk allocated three objects per traversal.
Decompiling System.Private.CoreLib that came to ~1.7 GB of extra garbage,
roughly +7% over the linked-list model the slot tree replaced. A yield
iterator removes the List but trades it for an equally costly per-node
state machine, so it is not enough on its own.
Enumerate children through a by-value struct enumerator over the existing
FirstChild/NextSibling primitives, capturing each child's successor before
it is yielded so a transform may still remove or replace the current child
mid-traversal. AstNodeCollection<T> gets the same struct treatment for a
direct foreach. Child enumeration now allocates nothing, bringing total
allocations back to the linked-list baseline at byte-identical output
(full Pretty suite green with CheckInvariant active).
Assisted-by: Claude:claude-opus-4-8:Claude Code
InsertMissingTokensDecorator removes a pending node by value and uses whether it
was still present, so a HashSet keys that membership-removal on O(1); iteration
order is irrelevant because every pending node receives the same location. Rename
the field to nodesAwaitingStartLocation for clarity.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A contributor guide next to the node classes: the [DecompilerAstNode]/[Slot]/
[ExcludeFromMatch] attributes, the slot-and-kind model (per-node CSharpSlotInfo
vs the canonical Slots constants), scalars and generated constructors,
CheckInvariant, and a step-by-step for adding a node.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The slot kind is now the canonical typed CSharpSlotInfo<T> in Slots, matched by
object identity (the IL SlotInfo model), instead of a parallel SlotKind enum.
CSharpSlotInfo.Kind points at the canonical shared slot; a Slots constant is its
own kind (null Kind, never read -- only per-node slots are asked for their kind),
so there is no self-reference. Child access (GetChild/GetChildren/SetChild,
GetCollectionByKind, AddChild/InsertChild) and the polymorphic
node.Slot.Kind == Slots.X comparisons key on the canonical reference; the
generated SlotKind enum is removed. No behavior change.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A declaration's attribute collection (AstNodeCollection<AttributeSection>) and an
attribute section's own attributes (AstNodeCollection<Attribute>) both carried
[Slot("Attribute")], so the kind mapped to two child types and the shared
Slots.Attribute widened to AstNode. Give the declaration-level slot its own
"AttributeSection" kind so every kind maps to a single type; EntityDeclaration's
Attributes accessor now reads through the typed Slots.AttributeSection. This was
the last child-access keyed on the SlotKind matching enum.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The generator emits a Slots holder of typed CSharpSlotInfo<T> kind constants (T is
the child type, widened to AstNode for the few kinds reused with several child
types). Call sites whose kind maps to a single type switch from
GetChildByRole<T>(SlotKind.X) to GetChild(Slots.X), inferring the result type from
the slot. The ByRole methods and SlotKind remain for the multi-type Attribute
accessor and the internal add/insert plumbing, removed in following steps.
Assisted-by: Claude:claude-opus-4-8:Claude Code
GetChild/GetChildren/SetChild take a typed CSharpSlotInfo<T> and infer the child
type from the slot, delegating to the existing kind-based lookups. Call sites can
pass a node's static slot (e.g. node.GetChild(PropertyDeclaration.GetterSlot))
instead of an explicit type argument plus a SlotKind.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Add a generic CSharpSlotInfo<T> (T is the child type, the element type for a
collection) and have the generator emit each per-node slot field as the typed
variant. This lets the typed child accessors infer the result type from the slot,
so the explicit type argument can drop out of the SlotKind-based child API in the
following steps. No behavior change -- the typed slots are still consumed through
the non-generic CSharpSlotInfo base.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A DEBUG-only structural check, the analog of ILInstruction.CheckInvariant, run
after every AST transform in RunTransforms. It recursively asserts that each
required (non-optional) single slot holds a child, every child's Parent points
back, the stored flattened index matches the slot position, and the runtime type
fits the slot, so a transform that corrupts the tree fails at that transform
instead of as a downstream output diff. CSharpSlotInfo gains an IsOptional flag
(emitted by the generator) so the check can tell required from optional slots.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Completes the nullable migration for ICSharpCode.Decompiler/CSharp/Syntax: every
hand-written file now carries #nullable enable. The pattern-matching API
(INode/Pattern/PatternExtensions DoMatch/Match/IsMatch) takes a nullable 'other',
since matching legitimately compares a pattern against a missing child. Members
that genuinely return or accept null are annotated to match (AttributeSection's
target token, IAnnotatable.Annotation<T>, Statement/Expression.ReplaceWith,
SyntaxExtensions.GetNextStatement), and a latent null dereference in
Backreference.DoMatch is fixed.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A blank line as the first line inside a node class body (right after the opening brace) or the
last line before the closing brace, left over from the migration. Cosmetic only -- deletions of blank
lines at class boundaries; method bodies are untouched.
Assisted-by: Claude:claude-opus-4-8:Claude Code
From a generated-code review. InterpolatedStringExpression.Content carried a leftover
[Slot("Role")] from the old role system -- the only generic 'Role' kind among 141 generated files;
it becomes [Slot("Content")], which also drops 'Role' from the generated SlotKind enum (nothing
referenced it). The generator's internal NameSlots/NullOnEmpty identifiers were stale -- the [NameSlot]
attribute and nullOnEmpty flag they were named after no longer exist (optionality is inferred from the
nullable annotation) -- so they become NameAccessors/IsOptional, matching the optional/required wording
used elsewhere. The *AstType visit-method rename uses EndsWith instead of a Contains substring match.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The flags word existed to back per-node bit state, but its only user was Identifier.IsVerbatim,
and it was retained as the seed for a deferred flag-packing optimization. Measuring that optimization
showed it saves essentially nothing -- the CLR already packs a node's bools and narrow enums into the
object's existing alignment padding (ParameterDeclaration and BinaryOperatorExpression: 0 bytes saved
by hand-packing). So IsVerbatim becomes a plain bool (free, it lands in padding) and the flags field is
removed. Removing it shrinks every AstNode by 8 bytes -- the field occupied its own aligned slot rather
than pairing with childIndex as assumed (a minimal node measured 64 -> 56 bytes), a universal win across
the whole tree that the per-node packing never delivered.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The C# syntax tree's name accessors (ParameterDeclaration.Name, GotoStatement.Label,
catch variable, query continuations, ...) are convenience strings over a backing Identifier
token. Names that may be absent are now typed string? and read as null when absent, instead of
a non-null empty-string sentinel; required names stay non-null string. The backing token slot's
nullability follows, and an empty or null assignment clears the token (empty == absent).
Because the property type now carries optionality, the separate [NameSlot] attribute and its
nullOnEmpty flag are redundant: a [Slot] on a string property is a name (child slots are
AstNode-typed, so the type disambiguates), and the generator infers optionality from the
declared nullable annotation -- which a string? declaration already requires #nullable for. The
repeated empty-to-null setter body becomes Identifier.CreateIfNotEmpty. To make the annotation
readable, #nullable enable is turned on across the syntax node files (the directive the inferred
optionality depends on), with the attendant local nullability fixups. Consumers that feed a name
into a non-null slot assert it where the variable structurally has a name.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Enum-typed constructor parameters were emitted via SymbolDisplayFormat.FullyQualifiedFormat so
that an enum in another namespace resolves without a using. For enums declared in the same namespace
as the generated file (ICSharpCode.Decompiler.CSharp.Syntax) that produced a redundant
global::ICSharpCode.Decompiler.CSharp.Syntax. prefix, e.g. 'global::...Syntax.ClassType classType'.
The prefix is now stripped for the file's own namespace while cross-namespace enums stay qualified.
The global:: on Identifier.Create in NameSlot setters is deliberately kept: those run in expression
position where a string property literally named 'Identifier' (e.g. SimpleType) would otherwise
shadow the type.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The generated 'public static implicit operator' line was emitted at two tabs, one more
than its own braces and the sibling PatternPlaceholder class, because the verbatim template
hardcoded the leading indentation. Emit it at one tab so the generated members align.
Assisted-by: Claude:claude-opus-4-8:Claude Code
GetChildByRole<T> returned default! while it can legitimately yield null (empty slot
or a kind the node does not declare); it now returns T?. Callers that already null-checked
are unchanged; the few that rely on a structural guarantee (name tokens, label identifier,
property accessors) now state it with an explicit !, and MethodDeclaration drops a redundant
cast that the lie required.
Also removes two members left dead by the Role removal: the AddAnnotation override that
only forwarded to the base, and GetCSharpNodeBefore, which ignored 'this' and had no callers.
On the output path, GetChildNodes() allocates a snapshot List per call; the read-only
.Children traversals that never mutate the tree now walk the live FirstChild/NextSibling chain
(O(n), allocation-free, since child indices are cached), an EmptyStatement child check uses
HasChildren, and the label-statement sibling scan breaks on the first match instead of walking
to the end of the block. The generated GetChildNodes snapshot is kept where traversal may mutate.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two crash paths surfaced by the null-object -> nullable-reference migration of the
C# AST. A void 'return;' forced into lambda syntax NRE'd on
ReturnStatement.Expression.Detach() now that Expression is nullable; the body now
falls through to the block form, matching the existing guards in CallBuilder and
InferReturnType. Separately, the location-setting token decorator routed a parentless
print-only node (e.g. the detached name-token clone of a renamed constructor) into the
child setter as SlotKind.None, which throws since the Role hierarchy was removed; such
nodes have no slot and are now skipped. A regression test covers the decorator path.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The per-node .g.cs files are read during debugging and review, so the emitter
now shapes them the way a person would: fixed-slot nodes dispatch child access
through a switch, collection nodes through a readable running-index walk (no dead
trailing decrement), GetChildCount folds its constant terms, and the fully
qualified System.Collections.Generic names collapse behind a using. The visitor
interface and SlotKind enum gain the auto-generated header and #nullable, and all
three writers normalize newlines to LF.
No behavior change: the dispatch logic is identical; only formatting and the
unreachable final decrement differ.
Assisted-by: Claude:claude-opus-4-8:Claude Code