Copying 'params' from every target delegate's Invoke onto the lambda broke
the Newtonsoft.Json round-trip: each plain '(object[] args) => ...' bound to
a 'params' delegate came back as '(params object[] args)', and below C# 12
the fallback rendered that as '([ParamArray] object[] args)', which no C#
version compiles (CS0674, plus CS8400 for lambda attributes before C# 10).
The lambda's own metadata is the faithful record for named delegate types:
current Roslyn emits ParamArrayAttribute and the default value on the
closure method exactly when the source spelled them, and where Roslyn 4.14
omits the attribute the plain parameter list is equivalent anyway. Only a
compiler-synthesized delegate type has to be spelled through the lambda's
declaration, so that is the one place Invoke is still consulted. Without a
legal pre-C# 12 spelling, the modifiers are now dropped instead of being
turned into attributes.
Assisted-by: Claude:claude-fable-5:Claude Code
C# 10 converts a lambda's function type to System.MulticastDelegate, its base
classes and its interfaces, and an expression tree's to Expression and
LambdaExpression, so 'Delegate d = (Func<int, int>)((int x) => x);' names a
type the language re-infers. The natural-type annotation now also marks
anonymous functions, and the declaration site drops the redundant cast.
The 'var' shortcut stays method-group-only on purpose: 'var' over an
expression tree infers the delegate that the Expression<> wraps, which would
silently turn a tree into a delegate. Discards are unaffected, since only a
declaration ever unwraps - a discard offers no target type, and function
types are not used in assignments to discards.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
C# 10 converts a function type to System.MulticastDelegate, to its base
classes and to its interfaces, but only the Delegate and object targets
dropped the explicit delegate construction. A local declared as
MulticastDelegate, ICloneable or ISerializable kept 'new Func<int, int>(M)'
even though a bare method group binds there just as well. Deriving the set
from MulticastDelegate's base types states the language rule directly instead
of listing two of its five members.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An anonymous function's own method does not reliably carry ParamArrayAttribute
- Roslyn 4.14 omits it - while the delegate's Invoke method always describes
the full signature, and it is Invoke that call sites bind against. Reading the
modifiers from the anonymous function alone therefore dropped 'params' from
the parameter list while the call was still decompiled in expanded form,
which does not compile: the natural type of the re-emitted lambda is a plain
Func<int[], int>, taking exactly one argument.
Sourcing both modifiers from Invoke also makes the pre-C# 12 downgrade to
[ParamArray]/[Optional] fire on every compiler rather than only the newest.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Ref-readonly-ness is not part of IType; it is a separate flag on the delegate's
Invoke method. The explicit-return-type gate compared types only, so a lambda
whose synthesized delegate returns 'ref readonly' matched its inferred natural
type and the modifier was dropped without a trace. Recompiling that output
synthesizes a plain ref-returning delegate, so the type identity silently
differs from the original.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A lambda was wrapped in a cast to its delegate type and a method group in a
construction of it, both of which later collapse away wherever the target type
is that same delegate. Where the target is a base type instead, such as a field
or return type of object or Delegate, nothing collapsed them and the wrapper
spelled out a type that has no name in C# and whose definition is hidden, so
the output did not recompile.
Neither wrapper can ever be written for an anonymous delegate type, so neither
is built now; the conversion rides on the anonymous function itself. That also
keeps the site typed as the delegate rather than as an anonymous function,
which is what lets the conversion to a base type proceed without a cast: the
standard permits it (C# 12 draft, 10.2.21) but the resolver does not model it.
Assisted-by: Claude:claude-fable-5:Claude Code
A delegate construction site can drop the explicit 'new DelegateType(...)'
only when the natural type C# assigns to the emitted method group is
exactly the delegate type the IL constructs; otherwise 'var' (or a
Delegate/object local's initializer) would re-infer a different type or
none at all. MethodGroupNaturalType re-resolves the emitted form and
decides this, mirroring the version-specific rules: C# 13 walks scopes
one at a time (instance members before each extension scope) and prunes
candidates with mismatched arity, violated constraints or the wrong
static/instance form; C# 10 lets every candidate in every scope take
part. Only System.Action/Func (or anonymous delegate) types qualify -
Roslyn never infers a signature-compatible custom delegate type.
CallBuilder annotates qualifying method groups; DeclareVariables uses
the annotation to emit 'var' when the natural type equals the local's
type, and to drop the construction (but keep the declared type) for
Delegate- and object-typed locals. Sites in any other context keep the
explicit construction. Generic groups retry with spelled type
arguments, since a natural type requires them.
Assisted-by: Claude:claude-fable-5:Claude Code
MethodGroupNaturalTypeImprovements gates the scope-by-scope candidate
pruning of C# 13 separately from the C# 10 natural-type support, so
decompiling as C# 10-12 keeps the C# 10 natural type rules.
Also make ExpressionBuilder and CallBuilder consult
NaturalTypeForLambdaAndMethodGroup before treating an anonymous
delegate type specially; DeclareVariables already did, so turning the
setting off previously produced an inconsistent mix of natural-typed
lambda syntax and unspeakable delegate type names.
Assisted-by: Claude:claude-fable-5:Claude Code
When LambdaOptionalAndParamsParameters is disabled, no anonymous
function syntax can declare 'params' or a parameter default value, so
these were silently dropped. Emit the underlying metadata attributes
([ParamArray], [Optional] plus [DefaultParameterValue]) on the parameter
instead, so the information stays visible. This has to happen in
TranslateFunction: from metadata alone the type system cannot tell a
lambda's method apart from a local function's, where the sugar stays
legal in older language versions, so neither a TypeSystemOptions flag
nor IsDefaultValueAssignmentAllowed can make this distinction.
RequiredNamespaceCollector adds System.Runtime.InteropServices for
optional parameters of method parts, since the downgrade decision is
made only later, during translation.
Assisted-by: Claude:claude-fable-5:Claude Code
A lambda body consisting of one ExpressionStatement was rendered as a
block, because only a single 'return' qualified for the expression form.
Any single statement-expression is a legal expression body - its value,
if there is one, is discarded and the lambda stays void-returning - so
braces are only needed for genuinely multi-statement bodies.
Assisted-by: Claude:claude-fable-5:Claude Code
Anonymous methods can never declare 'params' (CS1670) or parameter
default values (CS1065); since C# 12 lambdas can. TranslateFunction
printed both modifiers on whichever syntax it had chosen anyway, so
closure methods carrying ParamArrayAttribute or a default value
decompiled to uncompilable anonymous methods.
Force lambda syntax when a parameter carries one of these shapes, gated
by a new LambdaOptionalAndParamsParameters setting (C# 12). Below that
version the modifiers are dropped instead: the delegate type still
provides both, so they are purely decorative on the anonymous function.
The fixture branches per compiler because Roslyn 4.14 does not emit
ParamArrayAttribute on the synthesized lambda method (the modifier is
then unrecoverable), while current Roslyn does.
Assisted-by: Claude:claude-fable-5:Claude Code
A method group converted to a synthesized anonymous delegate type is
emitted as a natural-typed site ('var f = M;'). The delegate-reference
disambiguation only added generic type arguments when overload
resolution against the parameter types needed them, which models
inference from an explicit delegate target; with an anonymous delegate
type there is no target type to infer from, and a generic method group
without explicit type arguments has no natural type (CS8917). Force
the type arguments whenever the constructed delegate type is anonymous.
Assisted-by: Claude:claude-fable-5:Claude Code
Method groups and lambdas whose shape Action/Func cannot express (ref
parameter or return kinds, and with pointers, params, or default values
the non-generic fallback) get compiler-synthesized delegate types:
<>A{flags} (void-returning), <>F{flags} (value-returning), and
<>f__AnonymousDelegateN. Their names are unspeakable, so declared
variables printed the escaped type name and did not compile.
Detect them (generated name in one of the three families, delegate
kind, CompilerGenerated, no namespace), declare locals of such types as
'var', hide the synthesized type definitions, name the locals 'anon',
and let DelegateConstruction accept the synthesized methods. Since the
site is then typed solely by the anonymous function's natural type,
lambda syntax is mandatory: 'delegate {}' without a parameter list has
no natural type, and the 'delegate' form cannot declare a return type.
When the return type C# would re-infer from the emitted body differs
from the delegate's (a widened return, or a discarded value in an
expression body), the lambda declares the delegate's return type
explicitly (C# 10).
The LambdaReturnTypes fixture pins that last part: in a delegate-typed
context an explicit return type leaves no trace in metadata, so the
syntax only matters for natural-typed lambdas. Every case has a ref
parameter, which makes 'var' the only legal declaration, and the return
type appears exactly where it is load-bearing - widening the body's type
to object, or forcing void over an inferable int - and is omitted where
inference recovers it.
Gated by a new NaturalTypeForLambdaAndMethodGroup setting (C# 10).
Assisted-by: Claude:claude-fable-5:Claude Code
The C# 10 grammar only allows attributes on a lambda or its parameters
when the parameter list is parenthesized, but LambdaNeedsParenthesis
predates attribute support and only considered the single parameter's
type and modifiers. An attributed lambda whose parameter type is erased
for being anonymous therefore printed as '[My] a => a.X', which does not
parse. Latent since attributed-lambda decompilation was added: every
other attributed lambda has explicitly typed parameters, which already
force the parenthesized form.
Assisted-by: Claude:claude-fable-5:Claude Code
C# 10 allows a lambda to declare its return type; the natural-type
feature needs to emit one whenever the delegate type is dropped but the
return type cannot be re-inferred from the body (e.g. a body whose type
is narrower than the delegate's return type, or an explicitly
void-returning body whose statement-expression has a value).
LambdaExpression gains an optional ReturnType child and the output
visitor prints it before the parameter list, which the grammar then
requires to be parenthesized.
Nothing sets the return type during decompilation yet; the
LambdaReturnTypes fixture stays ignored until natural-type support
lands.
Assisted-by: Claude:claude-fable-5:Claude Code
Comments used to be child nodes flushed by InsertSpecialsDecorator when the
next node started printing, which put the marker of an init-only setter right
after the keyword. In the slot AST comments are leading/trailing trivia, so the
accessor's trailing trivia moved the marker behind the accessor body. The
placement cannot go back to trivia on the body either: the auto-property
transform drops the body, and the marker with it. The accessor now carries the
init-only fact itself and the printer writes the marker next to the keyword.
Assisted-by: Claude:claude-opus-5:Claude Code
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
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
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
The parameter-list-less anonymous method form is compatible with any
delegate signature, and C# code must rely on exactly that when a
delegate's parameter types cannot be named at the use site: IL, unlike
C#, permits a delegate signature to reference less accessible types.
Expanding such an anonymous method into a lambda would force the
unnameable type into a parameter list. Keep the delegate form, with its
parameter list dropped, when the parameters are unused and one of their
types is not accessible from the current context.
Assisted-by: Claude:claude-fable-5:Claude Code
Under UseLambdaSyntax, anonymous functions became lambdas only when an
expression body was possible; statement-bodied ones kept C# 2 delegate
syntax. Now every anonymous function whose parameter shape a lambda can
express uses lambda syntax; delegate syntax remains for ref/out/in and
params parameters and for pre-C# 3 language profiles.
Two latent issues surfaced by the wider lambda coverage: DeclareVariables
assumed an insertion point directly under a LambdaExpression is an
expression body it must convert to a block, which block-bodied lambdas
now violate; and anonymous methods declared without a parameter list
carry compiler-generated parameter names like '<p0>' that are not valid
identifiers, so the lambda's mandatory parameter list regenerates such
names from the parameter type: (object obj, EventArgs e) => ...
A side effect visible in fixtures: an explicit parameter list can make
a delegate-creation cast redundant that bare 'delegate' syntax needed
for overload resolution, e.g. new Thread((ThreadStart)delegate { })
becomes new Thread(() => { }).
Assisted-by: Claude:claude-fable-5:Claude Code
The guard added here reads the target of a member access to decide whether an
assignment may move into a field initializer, and it recognises the current
instance as a ThisResolveResult. Only one of the two spellings produces that.
An unqualified `A` is resolved through CSharpResolver.LookInCurrentType, which
synthesizes the target as a this-reference; an explicit `this.A` is built by
ExpressionBuilder, whose TranslateTarget hands back whatever ConvertVariable
produced - and `this` is a parameter like any other there, so the target is an
ILVariableResolveResult. The guard saw the first and missed the second.
Which spelling appears is decided by RequiresQualifier, for reasons unrelated
to the question being asked: a constructor parameter that shadows the field
forces the qualifier, and AlwaysQualifyMemberReferences forces it everywhere.
So the transform hoisted `b = this.value + 1` into a field initializer, where
naming the instance is CS0027 and the output does not compile.
TranslateTarget already builds a ThisResolveResult for `base`, one branch
above. Doing the same for `this` leaves the guard untouched and makes it see
both spellings, and spares every future consumer the same trap. The type is
carried over from the previous resolve result, so nothing downstream observes
a different one - the this/base keyword links read exactly this node.
Fixes#3984.
Assisted-by: Claude:claude-opus-5:Claude Code
A moved field initializer cannot read another instance member. Reject primary-constructor conversion for that case and preserve the original constructor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
Tuple element names and nullability are not part of a type's identity, so an
interface resolved through one of its members carries neither. Naming an
explicit implementation from that type produced `void I<(int, int)>.M()` on a
type declared as `I<(int A, int B)>`, which the C# compiler rejects outright
with CS0540 - the decompiled source did not build. The nullable case was
already recorded as a TODO in the NullableRefTypes fixture, where the mismatch
costs a CS8643 warning rather than an error.
The implementing type's base-type list is the only place those annotations are
recorded, so the qualifier is looked up there. Three call sites derived it
independently - the AST builder for all five member kinds, the ambience used
for tooltips and tree labels, and the forwarders synthesized for MethodImpls -
so they now share one helper rather than repeating the rule twice more.
Matching while ignoring tuple names and nullability cannot be ambiguous:
implementing two interfaces that differ only in those is itself an error
(CS8140, CS8645).
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
One member the decompiler could not handle aborted the whole export, so a
single unsupported method in a large assembly left the user with nothing: no
sources, no .csproj, no way around it. Recovering silently would trade that
for a worse outcome - broken output nobody knows is broken - so every failure
is recorded, written where the content would have gone, and pointed at the
issue tracker.
The recovery has to hold for anything the export touches, not just method
bodies: a file that cannot be created, a resource that cannot be decoded, an
output visitor that throws mid-type. Each of those costs its own unit and
nothing else, and the units behind a failure are still produced - dropping
them would make the export look complete when it is not.
Consumers that relied on the exception keep their failure signal: ilspycmd
exits non-zero and lists the failures, the PowerShell cmdlets raise an error
record per failure, and the round-trip suite asserts the export reported none
- otherwise a crash on a method its own tests never call would ship green.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A query source can be reached through an indexer as well as through a member
access or a call: `holder?[0].Where(...).Select(...)` puts an IndexerExpression
between the LINQ call and the `?.`. The receiver walk stopped there, so query
syntax was still introduced over a source the conditional access had lifted to
a nullable value type, and the output failed to compile with CS1936 - the same
way as the case that was reported, one node kind further along.
IndexerExpression.Target is nullable where MemberReferenceExpression's and
InvocationExpression's are not, so only that arm needs to match on the target.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Query syntax cannot preserve a null-conditional receiver that lifts a value type. Detect null conditionals through the LINQ receiver chain before introducing query syntax.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
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
The LanguageVersion setter's InvalidOperationException is a safety net
against exporting a project whose LangVersion cannot compile the
emitted code, but it only fires at assignment time: Settings is mutable
and shared, so enabling a feature after assigning the version slipped
past the check. Re-validating at the start of DecompileProject closes
that gap while keeping the setter's immediate feedback.
Assisted-by: Claude:claude-fable-5:Claude Code
A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.
Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.
Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.
Assisted-by: Claude:claude-fable-5:Claude Code
The VB compiler carries the range variables of a query in $VB$It, $VB$It1,
$VB$It2 and $VB$ItAnonymous, its counterpart to C#'s <>h__TransparentIdentifier.
Unrecognized, they were left in place by CombineQueryExpressions, and since '$'
is not legal in a C# identifier every VB query with more than one range
variable decompiled to code that cannot be recompiled.
Assisted-by: Claude:claude-fable-5:Claude Code
FractionApprox rejects inputs above 0x7FFFFFFF because they cannot be stored
as a fraction, but the check was one-sided while the sign is stripped right
after it. A large negative value therefore reached the continued-fraction loop
and overflowed the terms it accumulates. ICSharpCode.Decompiler is built with
CheckForOverflowUnderflow, so that aborted decompilation of the whole member
instead of wrapping.
Found by fuzzing nuget.org; reproduces on MathNet.Numerics, whose constants
reach the approximation through their ratio to PI.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
IsCopyConstructor required the copy constructor to be private on a sealed
record and protected otherwise, but IsGeneratedCopyConstructor in the same
class accepts protected regardless of sealedness. Since the former gates the
latter, a sealed record whose copy constructor stayed protected -- what you get
when a record is sealed after it was compiled -- was not recognized as one at
all: it fell through to the general constructor handling, where its base call
made it count as unchained, and the primary-constructor invariant then failed.
Found by fuzzing nuget.org. Beyond silencing the assertion this improves the
output, as the affected types now decompile to positional records.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The accessor forwarding-stub matcher bounded the IL body from above but not
from below. Reference assemblies keep the method RVA while stripping the body
to zero bytes, so an explicit interface accessor there sailed past the size
check and the first opcode read ran off the end of the blob, aborting the
whole type with a BadImageFormatException. The sibling matcher in
TransformDisplayClassUsage already guards its lower bound; this one did not.
Found by fuzzing nuget.org: every package resolving to the
Microsoft.NETFramework.ReferenceAssemblies packs was affected.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Roslyn compiles top-level statements into a synthesized Program class
whose entry point is called '<Main>$', a name that cannot be declared
in C#. Decompiled output kept it verbatim (escaped to
_003CMain_003E_0024 when exporting a project), and since C# accepts
only a method called 'Main' as an entry point, the exported executable
did not compile (CS5001).
Give that method the name 'Main'. Per the decision recorded in #829 we
do not reconstruct top-level statements, so this is the level of
support the output needs to compile.
An async top-level program needs the name in a different place: it
compiles to '<Main>$' holding the statements plus a '<Main>' entry
point that only awaits it. That wrapper carries the .entrypoint marker
but is hidden from the output, so the name goes to the method it
awaits instead - unless AsyncAwait is off, when the wrapper is
emitted and keeps the name itself.
Assisted-by: Claude:claude-fable-5:Claude Code
decimal has no IL literal: legacy csc compiles 0m to a Decimal.Zero
field load, which already decompiled to the literal, but Roslyn
compiles it to a zero-initialization, which decompiled to
default(decimal). The two forms are bit-identical for decimal, so the
literal is no less faithful to the IL and matches what a human writes;
it also removes the per-compiler split in the CompoundAssignmentTest
fixture.
Assisted-by: Claude:claude-fable-5:Claude Code
Operator precedence made the condition read
(setting && Add) || Subtract, so 'x -= 1' was converted to 'x--' even
with IntroduceIncrementAndDecrement disabled. Only observable with the
non-default setting, which no fixture configuration exercises, so no
test accompanies the fix.
Assisted-by: Claude:claude-fable-5:Claude Code
Reviving the 2020 test-cases-fp-types fixtures (compound assignment on
float, double and decimal) exposed an asymmetry: post-increment and
post-decrement on float/double round-tripped as x++/x--, but the pre
forms came back as x += 1f because the increment detection in
PrettifyAssignments only accepted integer constants. C# defines ++/--
on floating-point types as adding or subtracting exactly 1, so the
conversion is exact for a constant 1 operand. Decimal already works
through the op_Increment/op_Decrement path.
Assisted-by: Claude:claude-fable-5:Claude Code
Optimized code stores no temporary for a deconstruction element that is
used only once after the deconstruction. MatchAssignments handled that
for trailing elements, but a nested deconstruction copies the inner
element to a temporary, so the elements preceding it are also left
without an assignment; their external load then violated the
DeconstructInstruction invariant that all pattern variable loads are
descendants of the instruction. The forwarding fixup now covers all
unassigned elements and inserts in pattern order, because the statement
and expression builders pair pattern variables with assignments
positionally. This also fixes the nested tuple deconstruction crash
reported in #3388.
Also unwrap the address of the tested operand in
VisitDeconstructInstruction: deconstructing a struct passes the
receiver by reference, which was emitted as an invalid cast,
'var (x, y) = (S)(ref s);', even without nesting.
Fixes#3388.
Assisted-by: Claude:claude-fable-5:Claude Code
A class may name its own protected nested interface in its base list
(class F : F.IFoo), but referencing a protected interface nested in a
base class there (class SubF : F, F.IFoo) does not compile, even though
that interface is accessible inside the class body. The interface-impl
metadata still lists such interfaces (they are inherited through other
entries), so emitting every entry produced uncompilable sources. Skip
base types that are neither nested within the current type's nesting
chain nor accessible from the enclosing scope without the
protected-through-inheritance privilege. The check covers every type
the base-list reference names: type arguments, array and tuple
elements, and the declaring chain of the named type.
Assisted-by: Claude:claude-fable-5:Claude Code
The resolver comments cited section numbers from the C# 4.0 spec (and a
few from C# 9.0 drafts), which no longer match the published ECMA-334
standard. Renumber them against dotnet/csharpstandard draft-v11; every
reference was checked against the actual section headings. The old
'better conversion from type' subclause (7.5.3.4) no longer exists as
such and its rules live in 12.6.4.5-12.6.4.7, so that comment now says
so instead of pointing at a dead number.
Assisted-by: Claude:claude-fable-5:Claude Code
The .NET 10 BCL ships static [Extension] classes that contain ordinary
nested types (e.g. XDocumentExtensions.XDocumentNavigable). Decompiling
such a nested type's member in isolation resolved the enclosing
container's ExtensionInfo, and DecompileBody then dereferenced the
missing extension-member mapping. A container without any extension
blocks now reports no ExtensionInfo at all, and ResolveExtensionInfo
applies a container's info only to members that actually belong to one
of its extension blocks.
Assisted-by: Claude:claude-fable-5:Claude Code
* Make IProjectFileWriter implementations public and extensible
* Fix nullable error
* Fix delegate invocation to prevent race conditions
Refactored code to assign WriteCustomPropertyGroup and WriteCustomItemGroup delegates to local variables before null checks and invocation. This ensures thread safety by avoiding race conditions if the delegates are modified by other threads.
* Replace events with a GetCustomProperties virtual method
* Remove I prefix
The renamed-Implements case exposed a gap: the accessor-method
declarations did not include the explicit-interface-implementation
forwarders generated from .override directives, so decompiled types did
not implement their interfaces and same-name implementations lost their
interface mapping. DecompileParameterizedProperty now emits the same
forwarder stubs as the ordinary method path.
Assisted-by: Claude:claude-fable-5:Claude Code
The main tree, tooltips, and search results once showed the parameter
list of a parameterized property; the ambience lost that when property
rendering went through the converted AST node, whose C# property syntax
cannot carry parameters. Take the parameter list from the symbol
instead and render it in parentheses (matching VB.NET usage syntax and
distinguishing these properties from indexers).
Assisted-by: Claude:claude-fable-5:Claude Code
The compiler-generated documentation file contains a P: entry for a
parameterized property, but its accessors are emitted as ordinary
methods, whose M: id has no documentation entry. Fall back to the
owning property's documentation both when inserting XML documentation
into decompiled output (on the first accessor only, to avoid
duplicating it) and in the text view's tooltip (for either accessor).
Assisted-by: Claude:claude-fable-5:Claude Code
C# cannot declare a named property with parameters: only the type's
default member gets indexer syntax, and reusing it via [IndexerName]
collapses for types with several differently-named indexed properties,
static properties, or explicit interface implementations. Emitting the
accessors as ordinary methods is the only fully general compilable
form, matches how C# consumes such properties (Roslyn exposes the
accessors of properties it cannot bind as regular methods, the same
pattern C# 14 made user-facing for extension-member disambiguation),
and round-trips call sites to identical IL. Call sites already lower
to direct accessor calls.
The property-level attributes are kept on the first accessor under the
'property:' attribute target: it is not valid on methods, so csc emits
nothing for it (CS0657 warning) and recompilation neither loses the
attributes from the source nor misapplies them to the accessor. A
comment on the first accessor documents the deliberate deviation.
Visual Studio's metadata-as-source view drops such properties'
attributes entirely.
The assembly tree and tooltips are unaffected: they keep rendering the
property node with its parameter list. Known limitation, inherent to
any C# projection: recompiling the output produces plain methods, so
VB.NET consumers of the recompiled assembly lose property syntax.
Assisted-by: Claude:claude-fable-5:Claude Code