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
The 14 C# 1.0 settings each carried a handwritten
[Category("C# 1.0 / VS .NET")] literal, duplicating the per-version
display knowledge the generator's CategoryByVersion map single-sources.
Gating them on LanguageVersion.CSharp1 instead is observably identical:
CSharp1 is the smallest enum value, so the generated SetLanguageVersion
bucket can never fire, and the new GetMinimumRequiredVersion arm returns
the same CSharp1 the final fallback already does.
Assisted-by: Claude:claude-fable-5:Claude Code
The language version appears in two places that share a name but not a
concept, which repeatedly reads as one confused API: on
DecompilerSettings it is a construction shortcut (SetLanguageVersion
initializes the feature flags once and the version is not stored, so
the flags are the only state and the call is deliberately one-way),
while on WholeProjectDecompiler it is an export parameter (the
LangVersion stamped into the project file, defaulting to
GetMinimumRequiredVersion() and rejected below it as a safety net
against exporting uncompilable projects). Spell both roles out in the
XML docs so the distinction no longer has to be reverse-engineered.
Assisted-by: Claude:claude-fable-5:Claude Code
Four settings used the bare category string "Other" while the rest of
the group uses the "DecompilerSettings.Other" resource key. Both happen
to resolve to the same English text today, so the options UI shows one
group, but the two keys would split into separate groups the moment
their translations diverge.
Assisted-by: Claude:claude-fable-5:Claude Code
The setting carried the C# 11.0 display category but was missing from
both SetLanguageVersion and GetMinimumRequiredVersion, so decompiling
for an older target language version could still produce switches over
ReadOnlySpan<char> that the requested compiler cannot compile. Gating
it like the other C# 11.0 settings closes that gap; the category string
is now derived from the version like everywhere else.
Assisted-by: Claude:claude-fable-5:Claude Code
These three settings were disabled by SetLanguageVersion for older
targets but, unlike every comparable syntax-preference setting, never
raised GetMinimumRequiredVersion while enabled - an omission that had
gone unnoticed in the handwritten version bookkeeping. Drop the
AffectsMinimumRequiredVersion escape hatch that reproduced it.
Assisted-by: Claude:claude-fable-5:Claude Code
Every version-gated setting was bookkept in four places that had to stay
in sync by hand: the property boilerplate, SetLanguageVersion,
GetMinimumRequiredVersion, and the [Category] display string - and that
sync had already drifted in a handful of settings. A new source
generator in ICSharpCode.Decompiler.Generators now derives all four
from a single [DecompilerSetting] attribute on a partial property:
backing field, accessors with change notification, the version-derived
[Category], and both version methods. [Description] stays handwritten
because its resource keys are irregular and are grepped from the resx.
This commit is a 1:1 translation: the current inconsistencies are
reproduced exactly (AffectsMinimumRequiredVersion = false on
ExtensionMethods, UseLambdaSyntax and UseEnhancedUsing; no gate on
SwitchOnReadOnlySpanChar), verified against the old build by comparing
SetLanguageVersion and GetMinimumRequiredVersion behavior for every
setting at every language version, plus a reflection diff of the full
per-property attribute surface.
Assisted-by: Claude:claude-fable-5:Claude Code
Doc-comment folds were created with defaultCollapsed hardcoded to true,
so /// blocks always started collapsed. The new Display option is
bridged into DecompilerSettings like the other expand flags and drives
the fold's default state.
Detecting the last line of a doc-comment block requires sibling
navigation between trivia nodes, so trivia now carry parent, sibling
list, and index state. That state lives on the Trivia subclass to keep
AstNode's size unchanged, and the tree API treats trivia as a separate
navigation space: Next/PrevSibling and Remove work within the owning
list, Slot is null, ReplaceWith throws instead of aliasing the trivia
index into the parent's child slots, GetNextNode/GetPrevNode stop at
the list boundary, Clone and CopyAnnotationsFrom deep-copy and reparent
trivia, and CheckInvariant validates the trivia lists.
Assisted-by: Claude:claude-fable-5:Claude Code
Runtime async is a compiler feature that emits ordinary async/await (a
C# 5 construct), so reconstructing it should not require selecting C# 15.
The dedicated RuntimeAsync setting was also redundant: AsyncAwaitDecompiler
already runs the runtime-async transforms only when AsyncAwait is enabled.
Fold the behavior into the AsyncAwait setting and drop the separate toggle.
Assisted-by: Claude:claude-opus-4-8:Claude Code
When enabled, switch sections are ordered by their case label value
instead of by the underlying branch's IL offset. Default is false to
keep existing output unchanged. Useful when diffing decompiler output
across rebuilds of obfuscated assemblies, where IL block layout is
unstable but the case-to-value mapping is not.
Includes an ILPretty test that exercises a hand-written switch whose
table targets are placed at non-monotonic IL offsets (simulating
obfuscator block shuffling) and verifies the cases come out in
label-value order with the setting enabled. Also adds the
Resources.resx / Resources.Designer.cs entry so the WPF settings UI
shows a proper label instead of the raw key.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detect MethodImplOptions.Async (0x2000) in ILReader and unpack Task/Task<T>
return types so the IL Leave value and function.AsyncReturnType match the
source signature. Add CSharp15_0 (Preview also bumped to 1500) and a
RuntimeAsync setting (default on, gated to >=CSharp15_0), expose it in the
Languages dropdown, mask the synthetic MethodImplAsync bit out of the
decompiled [MethodImpl], and add a .runtimeasync test suffix.
Various improvements regarding primary constructor decompilation, including:
- introduce `HasPrimaryConstructor` property in the AST, as there is a difference between no primary constructor and a parameterless primary constructor
- improved support for inherited records and forwarded ctor calls
- exclude non-public fields and properties in IsPrintedMember
- introduce an option to always make the decompiler emit primary constructors, when possible
This setting is honored by all functionality that triggers a decompilation run. It is ignored by features that load assemblies as their primary function. For example, using the "Load Dependencies" feature will still resolve and load assemblies from the file-system. The same happens when you double-click on an assembly reference in the tree view. It will be resolved and loaded.
Note that disabling automatic assembly load will cause the decompiler to potentially not be able to resolve types from references that have not been added manually and the quality of the decompiled code will be inferior as a result.