Fix #3624: decompile semi-auto properties with the C# 14 field keyword - #3975
Open
siegfriedpammer wants to merge 3 commits into
Open
Fix #3624: decompile semi-auto properties with the C# 14 field keyword#3975siegfriedpammer wants to merge 3 commits into
siegfriedpammer wants to merge 3 commits into
Conversation
Covers field-backed properties across the feature surface: get-only, get/set, init and setter-only accessors, mixed explicit/auto accessors (bare get; / set;), property initializers, static properties, [field:] attribute targets, structs (incl. readonly accessors and ctor assignment to a get-only property), generic types, lambdas and local functions reading the backing field, and nullable annotations (null-resilient getters). The fixture is the desired output; it is excluded from the test-assembly compilation because its nullable annotations would trip warnings-as-errors there. The test is Assert.Ignore'd because the decompiler does not reconstruct the field keyword yet (#829). Both RoslynLatest debug/opt configs compile the fixture and fail only at the output comparison. Defects observed while probing: accessor bodies print the backing field by its metadata name, which is unspeakable, so default-settings output does not compile (the harness only survives via EscapeInvalidIdentifiers); in generic types the backing-field declarations are dropped entirely while their uses remain, because DoDecompileTypeDefinition's work list enqueues the specialized member but the output pass looks entities up by definition. Tester's .NET Core reference set gains System.ObjectModel.dll for INotifyPropertyChanged. Assisted-by: Claude:claude-fable-5:Claude Code
In generic types, resolve results reference members specialized by the type's own type parameters. The worklist dedupe and entityMap in DoDecompile(ITypeDefinition) are keyed by definition, so a hidden member re-added through the worklist (e.g. a property backing field referenced from an accessor) was decompiled under a key the output pass never looks up, silently dropping the declaration while keeping its uses. Assisted-by: Claude:claude-fable-5:Claude Code
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.
Assisted-by: Claude:claude-fable-5:Claude Code
siegfriedpammer
force-pushed
the
field-keyword
branch
from
August 9, 2026 18:22
e942708 to
e0f2c37
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3624 (also part of #829).
Property accessors that reference their compiler-generated backing field now decompile using the C# 14
fieldkeyword, and the backing-field declaration disappears. Compiler-generated trivial accessors collapse per accessor, so mixed shapes like{ get; set { if (field != value) { field = value; ... } } }come out naturally;[field:]attribute targets, property initializers (constructor stores lift to} = value;, and setter-less properties keepP = x;assignments in constructors), statics, init accessors, readonly struct accessors, generics, and lambdas/local functions readingfieldare all covered by the newFieldKeywordpretty fixture (compiled with Roslyn 5.9 at-langversion:latest).Design notes:
ExpressionBuilder.ConvertField: inside the owning accessor the backing field is printed as barefield(never qualified —this.fieldwould mean a real member). Recognition deliberately does not mirror the ILAst analysis used for automatic events: event recognition must prove compiler-generated bodies before discarding them, while the field keyword discards nothing, so name association plus accessor context suffices. AnAutoPropertyDecompilerconsolidation (records dedup, exact UI-tree hiding) remains possible as a follow-up without redesign.FieldKeywordsetting (C# 14.0, default on, via the[DecompilerSetting]generator). With it off — or below C# 14 — 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__BackingFieldto the readableP__BackingFieldinstead of the generic character escape. Exercised by a newNoFieldKeywordugly test.fieldis qualified asthis.fieldinside accessors, and locals are no longer namedfieldthere, because C# 14 rebinds the bare identifier (this fixed a real self-decompilation bug in ILSpy's ownTransformDisplayClassUsage).DoDecompile(ITypeDefinition)'s work list (in generic types the re-added declaration was silently dropped), and bodiless accessors mixed into multi-line properties now get their own line instead ofget; set {.Full ICSharpCode.Decompiler.Tests sweep on the branch: 3367 tests, 0 failed, 46 skipped (Windows-only/submodule-gated).
This PR was authored by an AI agent (Claude Code) operated by @siegfriedpammer.
🤖 Generated with Claude Code