Commit Graph
119 Commits
Author SHA1 Message Date
Siegfried Pammer e423cfb9ca Decide backing-field references by one verdict, not three
Three places decided independently whether a backing field would still be
declared, and they disagreed. ReplaceBackingFieldUsage rewrote a constructor
store into a property assignment whenever the property looked collapsible,
without asking whether PatternStatementTransform would actually remove the
declaration; ConvertField printed the "field" keyword on the FieldKeyword
setting alone, ignoring the GetterOnlyAutomaticProperties veto that
MemberIsHidden applies to the same field.

Two consequences, both silent. A setter-less property under
GetterOnlyAutomaticProperties = false kept its declaration and got "field" in
the getter anyway, so the keyword bound to a second synthesized field and the
declared one went unwritten. A settable property under AutomaticProperties =
false had its initializing store turned into a property assignment that
TransformFieldAndConstructorInitializers could no longer lift, leaving the
constructor in the output with an unconverted base-constructor call.

BackingFieldWillBeRemoved is now the single verdict every branch consults, and
it mirrors the transform's own entry gate. A property that keeps explicit
accessors is no longer addressed by name: the store stays a field reference and
becomes the property initializer, which is what field-backed storage means. The
one exception is a setter-less property's constructor store, which C# allows to
be written as an assignment and which has no other expressible form.

IsBackingFieldOfAutomaticProperty now answers through TryGetBackingField instead
of its own name check, so the two directions of "is this field that property's
storage" cannot diverge on staticness or field type. ReplaceBackingFieldUsage
dispatches on the resolve result rather than the identifier's spelling; after
ConvertField the same field appears both as "field" and under its metadata name
carrying the same annotation, so matching the name was matching the wrong thing.

The keyword's own precondition moves into a CanUseFieldKeyword local function.
Seven clauses with comment blocks wedged between them had to be read as one
expression; as one early return per rule the list reads in order.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 11:48:44 +02:00
Siegfried Pammer bdf48e5a47 Recognize field-backed properties when AutomaticProperties is off
ExpressionBuilder.ConvertField prints the C# 14 "field" keyword based on the
FieldKeyword setting alone, but PatternStatementTransform only entered the
property transform when AutomaticProperties was on. With FieldKeyword on and
AutomaticProperties off the backing-field declaration was therefore never
removed, and the output declared the backing field next to accessors already
written in terms of "field".

That output still compiles, which is what makes it dangerous: the keyword binds
to a second, freshly synthesized backing field while the declared one stays
unwritten, so the recompiled assembly has different storage than the input.
Only a CS0169 "field is never used" warning hints at it.

AutomaticProperties governs only whether trivial accessors collapse to
"get;"/"set;"; the declaration removal inside the transform is a separate step
that FieldKeyword alone is enough to justify. The entry gate now mirrors
CSharpDecompiler.MemberIsHidden, which already made the field's visibility
depend on either setting, with GetterOnlyAutomaticProperties vetoing the
getter-only case for both.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 11:48:44 +02:00
Siegfried Pammer 1e8ec6068b Keep VB auto-properties collapsing when the field keyword is on
VB emits an auto-property as a "_<PropertyName>" backing field plus accessors
it does not mark [CompilerGenerated]. The pre-C# 14 transform knows this: it
relaxes its accessor requirement whenever it finds such a field, which is how
a VB auto-property still prints as "{ get; set; }".

Routing every property through the field-backed path lost that. Collapsing an
accessor demanded [CompilerGenerated] unconditionally, so a VB auto-property
stopped collapsing and grew explicit "field" accessors instead - correct code,
but noise where every other compiler's equivalent stays a one-liner. Only the
legacy vbc configurations show it, and those run on Windows alone, so the
Linux and macOS jobs stayed green while both Windows ones failed on
VBPropertiesTest and Async.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-13 13:15:21 +02:00
Siegfried Pammer 3da129632e Fix #3624: decompile semi-auto properties with the C# 14 field keyword
Backing-field references inside a property's own get/set/init accessors
are emitted as the `field` keyword at IL-to-AST translation time
(ExpressionBuilder.ConvertField), so arbitrary accessor bodies become
expressible and no separate rewrite pass is needed. Compiler-generated
trivial accessors then collapse individually to `get;`/`set;`, which
also handles mixed shapes like `{ get; set { ... field ... } }`; the
backing-field declaration is removed with its remaining attributes
re-hosted as `field:` sections, and constructor stores become property
initializers (or property assignments, for setter-less properties).
Implicit zero-stores that auto-default struct constructors emit for
unassigned backing fields are dropped rather than lifted.

Recognition stays AST/metadata-based rather than mirroring the ILAst
analysis used for automatic events: events must prove compiler-generated
bodies before discarding them, while the field keyword discards nothing,
so name association plus the accessor context is sufficient.

Below C# 14 (or with the new FieldKeyword setting off), the field
declaration survives under its metadata name, so the UI keeps showing
the truth; EscapeInvalidIdentifiers - the transform the compilable-output
flows (project export, VS, tests) already add - now maps
`<P>k__BackingField` to the readable `P__BackingField` instead of the
generic character escape. A genuine field literally named "field" is
qualified as `this.field` inside accessors, and locals are not named
"field" there, since C# 14 rebinds the bare identifier. Bodiless
accessors mixed into multi-line properties get their own line in the
output.

The fixture covering the feature surface lands with the implementation rather
than as a separate xfailed commit. It is excluded from the test-assembly
compilation because its nullable annotations would trip warnings-as-errors
there.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 13:15:21 +02:00
Siegfried Pammer 7554c1b004 Remove the syntactic automatic-event patterns
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
2026-07-18 21:56:16 +02:00
Siegfried Pammer 461df88572 Replace automatic-event backing-field references during translation
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
2026-07-18 21:56:16 +02:00
Siegfried Pammer 1b9131905d Fix #3858: Detect automatic events on the ILAst instead of the C# AST
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
2026-07-18 21:56:16 +02:00
Siegfried Pammer f380b31a27 Fix #3804: reconstruct foreach over an inline array
The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.

Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-05 20:43:51 +02:00
Sebastien Lebreton 44ac0dd05d Address review: keep while-loop instead of headless for for ref locals
Per @siegfriedpammer: rather than rescue the hoisted for-initializer in
DeclareVariables, don't form the for-loop at all. TransformFor now bails when the
loop variable is a by-ref-like local used after the loop, leaving the while-loop
(matching source); the ref decl keeps its initializer, no CS8174. Reverts the
DeclareVariables change; test now expects the while form.

Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
2026-07-05 14:24:08 +02:00
Siegfried Pammer d3517739ff Add fine-grained C# AST debug steps
Record AST transform groups and mutation steps through the C# pipeline, replay selected steps with the stepper, and carry modified-node ranges through output so the Debug Steps pane can highlight the selected mutation without replacing its full step tree.

Assisted-by: CodeAlta:gpt-5.5:CodeAlta
2026-07-02 01:08:51 +02:00
Siegfried Pammer e264e926bb Enforce one child type per slot kind; de-alias shared kinds
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
2026-06-23 07:43:42 +02:00
Siegfried Pammer ed61b8a14e Replace the SlotKind enum with object-identity Slots kinds
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
2026-06-23 07:43:42 +02:00
Siegfried Pammer b0c8b7b2e5 Enable nullable reference types across the remaining C# Syntax files
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
2026-06-23 07:43:42 +02:00
Siegfried Pammer f5b00ac234 Model optional AST names as nullable; fold [NameSlot] into [Slot]
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
2026-06-23 07:43:42 +02:00
Siegfried Pammer 2ea569b005 Enable nullable reference types across the C# AST consumers
Turn on #nullable enable across the AST consumer layer: the output visitor, the
IL-to-C# builders (statement, call and expression builders, CSharpDecompiler,
TypeSystemAstBuilder), the translation-result wrappers, the sequence-point and
required-namespace collectors, and the annotation helpers. Optional inputs,
fields and returns are typed nullable, detector out-parameters use
[NotNullWhen(true)], and structurally-guaranteed dereferences use the
null-forgiving operator. A few public parameters that already tolerate null are
widened to match their downstream callers. The annotations emit no IL, so the
Pretty suite stays byte-identical.
Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-23 07:43:42 +02:00
Siegfried Pammer 9285a7328d Delete the Role hierarchy; key the child API on SlotKind
Slot identity already lived in node.Slot/CSharpSlotInfo/SlotKind after
the storage flip, so the parallel Role/Role<T> child model was redundant.
The Role-keyed mutation/query API is reexpressed over SlotKind, and the
role-index packing on AstNode flags is gone.

Because a node's Slot is now derived from its index in its parent rather
than stored on the child, the located-AST reattach in
InsertMissingTokensDecorator must capture the child's slot kind before
Remove() detaches it.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-23 07:43:42 +02:00
Siegfried Pammer 1f4fd68b3f Make optional AST slots nullable
Optional single-child slots return T? with a real null instead of a role
null-object, taking the C# grammar as the oracle for which slots are optional.
The generator emits the property type as T? and matches it with MatchOptional,
and consumers move from .IsNull to is null / ?.. This covers the optional
statement, member, try-catch, creation-initializer and pattern slots and the
optional NameSlot tokens. A few slots the grammar marks required but the
decompiler legitimately leaves empty (the implicit-element-access target, an
implicitly-typed lambda parameter's type) are flipped to nullable as well.
Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-23 07:43:42 +02:00
Siegfried Pammer 785e36712d Enable nullable reference types across the C# transforms
Turn on #nullable enable across the AST transform pipeline, ahead of
annotating the slot properties themselves. TransformContext now exposes the
nullable CurrentMember/CurrentTypeDefinition/CurrentModule contract already
declared by ITypeResolveContext, and the generated pattern-to-node conversion
returns a non-null node so patterns can be used in collection initializers
without warnings. No IL changes.
Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-23 07:43:42 +02:00
Siegfried Pammer 978c31ca5e Add PropertyAndEventBackingFieldLookup to improve performance of MemberIsHidden. 2025-06-15 16:19:59 +02:00
Siegfried Pammer c478ccc2e0 Fix #3319: KeyDownEvent field reference was replaced with KeyDown event reference. 2024-11-10 22:12:49 +01:00
Siegfried Pammer 9d22e3ab41 NullableLiftingTransform: Undo new compiler optimization (omitting get_HasValue for comparisions with constants) 2024-07-20 10:40:31 +02:00
ElektroKill a5febb3e4f Add support for VB.NET automatic events 2022-11-20 12:54:50 +01:00
Siegfried Pammer 40ffc1e90e Remove backing fields of auto properties and events.
The previous commit started decompiling backing fields because they are still needed prior to the C# transforms.
2022-04-29 16:28:32 +02:00
Siegfried Pammer f98012ec16 DetectPinnedRegions: Support special case where pinned pointer is unused and the compiler optimized out the StopGCTracking-conversion. 2022-03-07 09:19:26 +01:00
Siegfried Pammer ee3aabdc51 Fix #2534: Handle default implementations of properties and events in interfaces. 2021-11-03 22:05:01 +01:00
SilverFox 11b44e53b7 Add support for attributes of field/property in primary ctor of records 2021-09-24 19:27:06 +08:00
Siegfried Pammer bc5d078feb Fix #1698: Readonly auto properties from VB.NET are not properly decompiled 2021-06-06 20:18:18 +02:00
Siegfried Pammer ce199fad13 Fixed regression around foreach-on-string introduced in 16134e52e4 2021-05-26 23:58:15 +02:00
SilverFox 65ea2e7af6 Fix #2406: for readonly property with only getter, move readonly to property instead of getter 2021-05-11 11:14:35 +08:00
Siegfried Pammer 16134e52e4 Fix #2395: Do not use for->foreach transform if collection is not an array. 2021-05-07 23:23:18 +02:00
Siegfried Pammer aa147870a2 Fix #2389: missing extern keyword for properties and events. 2021-05-07 23:14:22 +02:00
Siegfried Pammer afa4db00b6 Fix #2297: Add an option to disable decompilation of getter-only auto properties. 2021-02-16 23:28:10 +01:00
dotnet format 0d9f871a4f #2128: Reformat the whole code base. 2020-08-28 01:05:38 +02:00
Siegfried Pammer 9e0e8d1cbc Add basic support for C# 8.0 enhanced using statements. 2020-08-23 16:42:30 +02:00
Siegfried Pammer 3535e01482 Fix regex for dotted property names. 2020-08-23 13:44:25 +02:00
Siegfried Pammer 7f9d9002bc #2104: Generalize auto property backing field name-checking 2020-08-23 12:42:34 +02:00
Siegfried Pammer 15028b3669 Fix #2104: Check field name of backing field when attempting to transform to an auto property. 2020-08-23 09:14:27 +02:00
Siegfried Pammer c9f5e5dd33 Refactor ForeachStatement to support deconstruction 2020-08-16 20:35:53 +02:00
Siegfried Pammer e22e5423d5 Fix #2098: Support auto event pattern with reordered arguments. 2020-08-12 17:55:53 +02:00
Siegfried Pammer 5b3c2f300a Merge branch 'master' of https://github.com/icsharpcode/ILSpy into issue1848 2019-12-08 09:39:42 +01:00
Siegfried Pammer 407c337168 Add ForStatement, DoWhileStatement and SeparateLocalVariableDeclarations settings. 2019-12-08 09:29:19 +01:00
Daniel Grunwald 703d21bafa Support C# 7.3 pattern-based fixed statement. 2019-12-07 21:31:00 +01:00
Siegfried Pammer b836f0b859 Fix #1837: auto events with tuples are not recognized. 2019-12-03 11:57:06 +01:00
Siegfried Pammer 941906e8db Fix #1699: Auto properties without getter are not properly decompiled
Also fixes wrong decompilation of auto properties modified by Fody.
2019-10-01 22:09:47 +02:00
Daniel Grunwald 8f90f3834d Implement support for C# 8 readonly members. 2019-09-28 19:13:21 +02:00
Siegfried Pammer 8d2e8cc267 Fix #1628: Preserve order of child nodes in PatternStatementTransform.VisitBinaryOperatorExpression. 2019-08-12 09:16:05 +02:00
Siegfried Pammer 13fa499c35 Fix #1630: Do not convert while to for statement, if any iterator variables are to be declared in the loop body. This causes problems with ref-typed variables. 2019-08-10 19:19:52 +02:00
Daniel Grunwald b75c252193 Upgrade C# 8.0 nullability support for Roslyn 3.2.0-beta4. 2019-07-24 23:10:33 +02:00
Daniel Grunwald 856cedc95e #1456: add test case; add additional checks to ensure we only transform normal locals into using/foreach-locals 2019-07-23 23:52:31 +02:00
Siegfried Pammer b0cb976357 Fix #1454: Name collision after converting for-over-array to foreach. 2019-03-17 19:14:23 +01:00