Making a conversion implicit by unwrapping it hands the operand to a
different target type, and a default literal takes its value from that
type: "S? x = new S?(default)" holds a value, while "S? x = default" is
null. Unwrapping the nullable constructor around a shortened literal
therefore turned "S? x = default(S)" into a null nullable. The literal is
spelled out again whenever unwrapping moves it to a type other than the one
it was shortened from.
Converting a using resource to the declared variable type is unconditional
now (except when the declaration says "var", which supplies no type): the
declaration always spells the type out, so any conversion to it may stay
implicit, which is also what shortens default(T) there.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Mutating the DefaultValueExpression is enough here; ConvertTo already hands
out mutated input nodes elsewhere (UnwrapChild), so building a replacement
node and copying the annotations over bought nothing.
The operator special case is easy to mistake for a cosmetic preference,
because the null literal is accepted in the same position: it converts only
to reference and nullable types, so it still narrows operator overload
resolution, whereas the default literal converts to everything and C#
rejects it outright for every binary operator except == and !=.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Shortening default(T) is the same problem as removing the redundant cast
around a lambda whose delegate type the context already fixes, so it uses
the same mechanism: ConvertTo makes the explicit type implicit when the
conversion is an identity conversion and the caller allows an implicit
one. The literal keeps the type it was shortened from, so any later
conversion to a different type - or any context that requires an explicit
type, such as an overload resolution recheck falling back to CastArguments
- can spell default(T) out again. That keeps the value intact where the
bare literal would change it, e.g. "object o = default(SomeStruct)", which
boxes a non-null struct while "default" would be null.
Because the shortened literal resolves to DefaultLiteralResolveResult,
CallBuilder's existing overload resolution recheck sees a real default
literal and rejects ambiguous calls on its own; no separate bookkeeping
about which arguments may stay untyped is needed. Only the contexts that
supply no target type at all restore the explicit form: an awaited
expression, and arguments of operator methods, which later become operator
or cast syntax rather than calls.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
There's an additional local variable when decompiling the non-optimized code; and explicitly putting that variable
into the test case just makes it fail due to yet another additional variable.
An operand boxed for the GetAwaiter call is typed 'object', so the member
lookup that decides whether the await needs a cast finds nothing and a
redundant cast to the receiver type reaches the output. C# inserts that boxing
conversion implicitly, so the box may be dropped -- but only after the lookup
confirms the unboxed operand still binds the same GetAwaiter, and only via the
resolve result: UnwrapChild detaches the operand from the AST, so running it
speculatively leaves a cast with no child behind and decompilation of the whole
method falls back to the raw state machine.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
I think this isn't reliable enough yet to actually omit parameter types for lambdas (it only protects against switching to the wrong overload; not against type inference failures); so for now it's only used in the query expression transform.
The sign of a constant cannot decide whether to emit a unary minus: MinValue
and NegativeInfinity are negative, yet are their own members and must not be
negated. Deriving it that way emits -float.MinValue for float.MinValue, which
is a different value.
Which constants are reachable by negation is also not obvious: -MaxValue is
exactly MinValue and both infinities have their own members, so Epsilon is the
only one, but establishing that takes a proof rather than a read. Recording it
in the table states the invariant instead, and leaves the lookup itself as the
single dictionary probe it was before.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An anonymous parameter type cannot be named, so the original lambda must
have used implicit parameters throughout. Emit the whole parameter list
implicitly instead of mixing implicit and explicit declarations.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
A negative constant operand is usually the two's-complement rendering
of a bit mask or a high unsigned value (an enum member, a sentinel);
the IL view now appends the hexadecimal form as a comment, e.g.
'ldc.i4 -501 // 0xfffffe0b' (#1142). The short forms stay bare: their
operand range is readable as-is. The disassembler round-trip comparer
strips comments, so the new NegativeConstants case pins the rendering
with explicit content asserts.
Assisted-by: Claude:claude-fable-5:Claude Code
An addition or subtraction on an enum whose constant operand's numeric
value does not fit the underlying type (an int constant standing for a
high uint member, e.g. -501 for 0xfffffe0b) failed to resolve as enum
arithmetic and fell back to integer arithmetic with casts, producing
'(uint)((int)value - -501)' and, for the same source expression in an
argument position, '(uint)value - 4294966795u'. Retry the failed
resolution once with constant operands reinterpreted in the enum type;
the reinterpretation is lossless whenever the constant's stack type
matches the enum's underlying stack type, because the IL constant is
the member's bit pattern. Valid non-enum resolutions like 'data - 1'
(enum minus underlying, yielding the enum) are unaffected because the
retry only runs when the plain resolution fails.
Assisted-by: Claude:claude-fable-5:Claude Code
Auditing against the first-class-span-types proposal turned up three
deviations, each now pinned by resolver unit tests whose expectations were
established by compiling probe programs with the C# 14 compiler.
Lower-bound type inference recursed into Span<T> targets as another
lower-bound inference, but Span<T> is invariant and the spec demands an
exact element inference there: M<T>(Span<T>, T) with (Span<string>, object)
must fail inference (CS0411), not unify to T=object.
Better-conversion-target compared ReadOnlySpan element types where the spec
compares the span types, admitting numeric and user-defined element
conversions the span types do not share: overloads taking ReadOnlySpan<int>
and ReadOnlySpan<long> are ambiguous (CS0121), not resolvable. The general
mutual-convertibility rule already implements the spec's span-type test, so
the element-level block is simply removed; the ReadOnlySpan-over-Span
identity rule stays, since it deliberately inverts that general rule.
The explicit span conversion did not exist at all, and with it the rule that
user-defined conversions are not considered between span-convertible types.
The visible consequence: string[] to Span<object> classified as an implicit
user-defined conversion via op_Implicit(object[]) plus array covariance,
where the compiler reports CS0266 - only the explicit span conversion
exists. Span conversions are also no longer considered for extension
receivers during method group conversion (CS0123), while invocations keep
them.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# 14 compiler lowers implicit span conversions to calls -
MemoryExtensions.AsSpan(string), ReadOnlySpan<T>.CastUp, and the span
op_Implicit operators - so decompiled code showed the lowered form even
though the conversion and betterness layers already implement the C# 14
rules. CallBuilder now folds those helper calls back into conversions,
riding the existing mechanism: the conversion is built as an explicit
cast, consumption sites make it implicit where the context allows, and
the overload-resolution recheck re-adds a cast when the bare argument
would bind to a different overload (which canonicalizes deliberate
AsSpan disambiguations to the equivalent explicit span cast).
Span conversions compose, so CastCanBeMadeImplicit lets a direct
input-to-target span conversion replace a chained pair; and an rvalue
bound to an in parameter gets the same chance to shed the cast as a
by-value argument, since ChangeDirectionExpressionTo bypasses the
by-value strip.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
Below C# 7 ref locals are unavailable, so CopyPropagation is allowed to copy
LdFlda/LdElema. When such a copy lands in a StObj target slot whose value is
impure, it violates the invariant checked by StObj.CheckTargetSlot: C# computes
the value to be stored before dereferencing the target, so the exception moves.
ILInlining resolves the same conflict by marking the address as delayed rather
than falling back to a ref local; copy propagation now does the same, which
keeps the generated code unchanged and only repairs the IL.
Unlike inlining, copy propagation has no third arm to fall back to: by the time
DoPropagate runs, the defining store is about to disappear, so every load has to
be replaced and refusing the copy is no longer an option. That decision can only
be made up front, which is what CanPerformCopyPropagation does when ref locals
are requested. The assertion covers the remaining hole, the public Propagate()
entry point, which bypasses that check -- AsyncAwaitDecompiler copies an ldflda
of the builder field through it irrespective of the setting.
Propagating an address also un-inlines its arguments into fresh stack slots, and
those stores are copy-propagation candidates in their own right. They are
inserted before the store being replaced, so the block scan used to step right
past them: a slot loaded more than once could never be inlined back and survived
into the output as a ref local -- for `s.ShortField >>>= 5` the copied ldflda
left behind `stloc C_0(ldloca s)`, printed as `ref CustomStruct reference = ref
s`. Rewinding the scan to the first of those stores lets them propagate too.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
When fixing a type parameter, Roslyn merges the tuple element names of
bounds that are identical apart from those names: names are kept where
all bounds agree and dropped where they conflict (MergeTupleNames in
Roslyn's MethodTypeInference.cs). The C# standard does not describe
this step. Without it, fixing either kept the first bound's names
verbatim or, with two exact bounds differing only in names, failed
outright - so inferred tuple types could carry names csc would not
produce. All merged-name expectations are csc-verified.
Nullability is deliberately not merged: Roslyn derives it from the
variance of the position, which this implementation does not track, so
bounds that differ in it stay distinct and fixing fails as before
rather than inventing an annotation.
Assisted-by: Claude:claude-fable-5:Claude Code
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
'this' and 'base' both read the 'this' parameter of the function being
decompiled, but their resolve result did not say so: consumers that key on
ILVariableResolveResult (local-reference output, highlighting, hover) could
not connect the keyword to the variable, and the qualified/unqualified
spellings of the same access carried differently shaped annotations.
The resolver has no ILFunction and thus no variable to put into a
ThisResolveResult, so it stops synthesizing one: LookInCurrentType looks
the name up against the (self-parameterized) current type, which grants
the same protected access, and the annotation of an unqualified field
access is built from the translated target instead. ResolveThisReference
and ResolveBaseReference had no callers left and are removed.
Assisted-by: Claude:claude-fable-5:Claude Code
A cast must not reuse an implicit tuple conversion: its elements have to be
classified as cast conversions, which changes the outcome whenever an element
converts through a user-defined operator. Roslyn encodes the same rule in
ClassifyConversionFromTypeForCast via ExplicitConversionMayDifferFromImplicit,
but on our side it rested on an unexplained flag with nothing covering it, so
the flag read as removable. The comments and the test say why it stays.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
TypePair existed only to key that cache, and its hand-written equality
delegated to the same IType comparison the tuple's default comparer performs.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The resolve-result hierarchy was split between ICSharpCode.Decompiler.Semantics
and ICSharpCode.Decompiler.CSharp.Resolver, so consumers had to know which half
a given result came from and import both namespaces. All subclasses now live
next to their base class; MethodListWithDeclaringType follows the method group
it describes, and ILVariableResolveResult gets its own file instead of sitting
among the syntax-tree annotation helpers.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Nothing walked the resolve-result graph: the virtual method and its fourteen
overrides only ever called each other, with InvocationResolveResult's chained
base call as the sole call site in the tree.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Both types are consumed well outside the C# output layer - DecompileRun
carries the using scope, and the IL transforms build a resolve context from
it - so living in ICSharpCode.Decompiler.CSharp.TypeSystem misrepresented
where they belong and forced a C#-specific namespace import on every
consumer.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The resolver's await path has thrown NotImplementedException ever since the
type system rewrite, and nothing else in the repo constructs an
AwaitResolveResult, AliasTypeResolveResult or AliasNamespaceResolveResult:
the decompiler builds await expressions from IL, and alias references never
go through name resolution.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A call to a value-type constructor is rewritten into
"stobj(target, newobj ...)" because "Struct.ctor(target, ...)" has no C#
equivalent. The rewrite keyed on TypeKind.Struct, so a struct from a
missing assembly resolved as TypeKind.Unknown, fell through to the
ordinary call path and produced a stack-type mismatch.
Metadata cannot settle the question: a TypeRef parent carries no valuetype
bit. The receiver can, though - a constructor invoked with "call" on an
address is a shape only a value type has - so the unresolved case follows
the receiver's stack type and steps aside where metadata does say the type
is a reference type. Reading the receiver has to leave it on the stack for
PrepareArguments, hence the depth-indexed peek.
Assisted-by: Claude:claude-opus-5:Claude Code
A writable ref-struct argument can receive narrower values through regular ref/out calls, and a ref-return can expose the same storage for field mutation. Treat those paths like receiver captures so inferred declarations remain compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d30b7a7-983d-4efa-8d99-fbface5828dc
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
Local scopedness is erased from IL and PDBs, so it can only be recovered
from the body. Compare each declaration initializer with later assignments,
field stores, and receiver captures using the C# 11 ref/value escape rules,
and emit scoped only when a later operation is strictly narrower.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
ScopedKind is now the authoritative lifetime representation, so retaining
the preview-era boolean fields would duplicate state. Keep the current
ScopedRef compatibility property and group the new metadata attributes
with the other C# 11 attributes.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
ScopedRefAttribute only records explicit syntax. Effective lifetime also
depends on UnscopedRefAttribute, params collections, out parameters, and
the defining module's RefSafetyRules version. Model those distinctions in
the type system without changing decompiler output.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
Overload resolution reports no error for an empty candidate set - there is
no best candidate to attach one to - so the null result passed for success
and was dereferenced while checking the call target. Decompiling
FSharp.DataFrame from nuget.org crashes that way: F# compiles its comparison
members to instance methods carrying operator metadata names, and the
operator candidate search looks at the operand types rather than at the
receiver type the member belongs to.
The new fixture pins that such an assembly decompiles at all. It still
renders those instance methods as operators and drops the receiver at the
call sites, which is the misclassification behind the empty candidate set
and is handled separately; this is the guard that keeps an empty candidate
set from being read as a resolved call.
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
Element 8+ of a long tuple is read through the Rest field, which Roslyn
loads by value; ILSpy turned that into an addressof over the loaded
copy, hiding the tuple field chain from every downstream matcher. Elide
the copy when the enclosing expression only reads through it - the read
then goes directly through the original address and folds into the
usual Item_N chain. The deconstruction transform's use-shape guard also
learns to walk that chain; whether each read really is a consumable
element access remains the job of MatchTupleElementRead and the escape
check.
Assisted-by: Claude:claude-fable-5:Claude Code
Deconstruction assignment copies the right-hand side into a temporary
before calling Deconstruct. When the RHS is a call, inlining folds that
temporary away, but for a local or parameter it survived into the
output as a separate assignment statement. Consume the copy into the
deconstruction pattern; rendering the copied value as the RHS
recompiles to the identical temporary. Because blocks are processed
back to front, the call-position match defers to the attempt starting
at the copy, mirroring the existing nested-deconstruction defer guard.
The new fixture also covers deconstruction assignment to locals
captured by a lambda in an async method (issue #3037's crash shape,
already fixed earlier).
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
* Set v11 RTM
* Update features in README.md
* Remove the two 900-iteration process-list scroll tests
* Keep Svg.Controls.Skia.Avalonia at 12.0.0.13
* 10.0.11 and Roslyn for net11p7
* Fix module-scan test failing when PowerShell's NGen images are stale
* Opt Pack NuGets out of the MSBuild server to fix SBOM generation
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