Skip to content

Fix #3624: decompile semi-auto properties with the C# 14 field keyword - #3975

Open
siegfriedpammer wants to merge 3 commits into
masterfrom
field-keyword
Open

Fix #3624: decompile semi-auto properties with the C# 14 field keyword#3975
siegfriedpammer wants to merge 3 commits into
masterfrom
field-keyword

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Aug 9, 2026

Copy link
Copy Markdown
Member

Fixes #3624 (also part of #829).

Property accessors that reference their compiler-generated backing field now decompile using the C# 14 field keyword, 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 keep P = x; assignments in constructors), statics, init accessors, readonly struct accessors, generics, and lambdas/local functions reading field are all covered by the new FieldKeyword pretty fixture (compiled with Roslyn 5.9 at -langversion:latest).

Design notes:

  • Emission happens at IL-to-AST translation time in ExpressionBuilder.ConvertField: inside the owning accessor the backing field is printed as bare field (never qualified — this.field would 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. An AutoPropertyDecompiler consolidation (records dedup, exact UI-tree hiding) remains possible as a follow-up without redesign.
  • New FieldKeyword setting (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__BackingField to the readable P__BackingField instead of the generic character escape. Exercised by a new NoFieldKeyword ugly test.
  • A genuine field literally named field is qualified as this.field inside accessors, and locals are no longer named field there, because C# 14 rebinds the bare identifier (this fixed a real self-decompilation bug in ILSpy's own TransformDisplayClassUsage).
  • Two independent fixes surfaced by the feature: referenced members are enqueued by definition in 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 of get; 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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

field keyword in auto properties

1 participant