EscapeIdentifier runs for every identifier token emitted, yet it built
a StringBuilder plus a fresh result string even for the overwhelmingly
common case of an identifier with no escapable characters. A pre-scan
now returns the original instance untouched in that case, and the
surrogate-pair copy appends the two chars directly instead of cutting
a two-char substring. New unit tests pin the escaping behavior and the
identity fast path.
Assisted-by: Claude:claude-fable-5:Claude Code
SplitTypeParameterCountFromReflectionName allocated the digits after
the backtick only to feed int.TryParse and discard them, and the
TopLevelTypeName constructor cut the name part twice for generic
types (once at the dot, again at the backtick), dropping the first
cut. Both run for every generic reflection name parsed, e.g. for
typeof-valued attribute arguments and string-switch metadata. The
arity is now parsed in place with a digit loop (netstandard2.0 has
no span int.TryParse) and each final string is cut exactly once.
The digit loop only accepts plain ASCII digits, so suffixes like
`+1 that int.TryParse tolerated are now rejected; such names are not
legal reflection names. New unit tests pin the parse edge cases.
Assisted-by: Claude:claude-fable-5:Claude Code
The regression only needs the nested catch inside an enclosing handler from #2829. Removing the explicit try/finally keeps the fixture focused on the failing control-flow shape instead of compiler-generated cleanup details.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
A catch inside an enclosing EH region can leave to a try body that immediately branches to the loop continuation. Treat that leave as a continue so decompilation does not expose the synthetic end label for the catch path.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Resource export sometimes needs framework-specific ResX type names. Exposing the WinForms and mscorlib assembly suffixes keeps the writer default-compatible while letting callers choose older resource type names when needed.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
ResX export should still produce a valid file when no resources were added and the writer is finalized through Close or Dispose. Calling Generate from Close preserves creation timing while matching the empty-file behavior.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
VB can lower event-handler closures through generated wrapper methods, closure copy constructors, and immediately invoked VB anonymous delegate helpers. Recognizing and unwrapping those shapes leaves the expected lambda form and lets captured locals be scalar-replaced instead of exposing generated closure artifacts.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Dynamic member tooltips should describe the visible static target, while punctuation should stay reserved for useful call targets such as constructors. This keeps dynamic object creation tooltips on the constructor parentheses but avoids noisy synthetic member references on dynamic invocation parentheses.
Assisted-by: OpenAI:openai/gpt-5.5:OpenCode
Roslyn can lower mostly constant arrays with a static data blob and leave non-constant elements as explicit stores. Merge those trailing stores into the decoded initializer so the decompiler keeps a single array initializer instead of splitting the expression.
Assisted-by: OpenCode:openai/gpt-5.5:opencode
Metadata can contain private virtual members that C# cannot declare directly. Adjusting the emitted accessibility in the decompiler keeps such members compilable without pushing the policy into the general AST builder.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
AccessedThroughProperty-backed controls need to remain addressable as fields so WinForms initialization can use compiler-generated backing storage without hiding the setter that maintains WithEvents subscriptions.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Captured locals can first be named while visiting a nested local function even though their declaring ILFunction is the parent. Reserving such generated names only in the nested scope lets sibling captured locals reuse the same name and later collide when declarations are hoisted.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Null propagation must not render an extension-method conditional access when normal member lookup would bind to a non-method member, because that produces invalid C# for ambiguous Count-style cases.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Recognize Roslyn's null-terminated backing-store shape for u8 literals so escaped control bytes decompile back to UTF-8 string literal syntax, fix LdStrUtf8 to infer ReadOnlySpan<byte> in both the template and generated code, and cover overload-resolution cases where the explicit span conversion must be preserved.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Roslyn annotates record copy constructors with SetsRequiredMembersAttribute when records have required members. Treat that attribute as allowed only on constructor-owned synthesized record members so copy constructors are recognized and required init auto-properties do not leak backing-field accesses.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Fix#4174 by bailing out of expression-tree call reconstruction when a converted argument cannot be built, and allow conditional operands that unify through an implicit reference conversion.
Assisted-by: OpenAI:gpt-5.5:OpenCode
Unary minus over a negative literal printed as adjacent minus tokens, which C# parses as pre-decrement instead of numeric negation. Treat this literal shape as requiring parentheses so the output remains legal and preserves the IL semantics.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
The command-line summary already names the failing token and member, but the generated code only asked users to report a generic exception. Put the same member context into the emitted comment so copied snippets identify the failing method.
Assisted-by: OpenAI:gpt-5.5:OpenCode
Some assemblies preserve the C# backing-field naming pattern and accessor shape but omit CompilerGeneratedAttribute from the field. Use the existing property/backing-field metadata lookup so those properties can still collapse to field-backed or automatic property output instead of leaking <Name>k__BackingField.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
AGENTS.md is the vendor-neutral name other coding agents already read,
and Claude Code now loads it through its built-in agents-md plugin
whenever a project has no CLAUDE.md of its own. One set of instructions
then serves every agent instead of only Claude. The plugin stands down
as soon as any CLAUDE.md remains in the project, so both files move
together rather than one at a time.
Assisted-by: Claude:claude-fable-5-1:Claude Code
Extension call syntax had no fixture of its own; it was covered only
incidentally by QualifierTests, Issue1080, RefLocalsAndReturns and a few
others. The cases that decide how the call is spelled - null receiver,
ref/in receiver, a named argument after the receiver, type arguments
inference cannot reach, params expansion, and an instance method that
beats the extension so the call has to stay static - now have one place
that fails if any of them moves.
Assisted-by: Claude:claude-opus-5:Claude Code
An extension method reference binds its receiver as the target, so the
delegate it is converted to has one parameter fewer than the method.
PerformOverloadResolution prepends the target itself when extension
methods are allowed, so handing it the receiver's parameter as well left
it with one argument too many: every candidate came back
TooManyPositionalArguments, the reference was reported ambiguous, and the
escalation ran to the end and wrote out type arguments that inference
could have supplied.
Two fixtures pinned that output. Both spellings compile to byte-identical
IL - Use2(ints.Select) binds Enumerable.Select<int32, int32> and the same
Use2 overload as Use2(ints.Select<int, int>), and .Any is the same
Enumerable.Any<int32> as .Any<int> inside the expression tree - so the
shorter one is what the escalation is supposed to arrive at.
Assisted-by: Claude:claude-opus-5:Claude Code
An index initializer is written as an element access even for a
parameterized property, which has no access syntax of its own, so
BuildDictionaryInitializerExpression routes one through the accessor
setter path regardless of symbol kind. The guard that stops an access
from dropping every argument only recognised indexers, so an index whose
argument repeats its declared default was left out, the element access
collapsed to a member assignment, and the placeholder target leaked into
the output. VB emits exactly this shape.
Which parameters carry the names, and which names can be written, were
decided in two places: NamedArgumentTransform admitted a reordering when
the accessor's parameters all had some name, while CallBuilder wrote the
names from the indexer's parameters and only when they were valid
identifiers. Where the two disagreed the reordering was kept and the
names dropped, leaving the arguments positional in the reordered order -
a silent change of meaning. Both now read one list under one predicate,
and a call whose names cannot be written is not reordered at all. The
value parameter is no longer consulted, since it is never named; COM
interop declares indexers that leave it unnamed.
The argument-list narrowing in Apply(CastArguments) is dropped again:
every step list runs NoOptionalArgumentAllowed first, so nothing is ever
left out by the time the casts go on.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An argument that repeats its parameter's default value may be left out,
but the value was only ever compared against the method the call
instruction names - for a virtual call the base declaration, since that
is the slot the compiler emits. The shortened form binds against the
receiver's static type, where an override is free to declare a different
default, and the recompiled code then passes that one instead. Calls have
had this since optional arguments were introduced; opening indexer
accesses to omission brought it to element accesses too.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
C# allows named arguments in an element access, but NamedArgumentTransform
refused to introduce one for any accessor, so an access whose arguments the
compiler reordered came out as a temporary. Only indexers gain this: a
property access has no argument list, an operator cannot take names, and a
setter's value stays unnamed on the right-hand side, which also keeps it last
in argument order for everything that identifies it by position.
Introducing a name replaces the call with a block, so it is refused where the
surrounding instruction requires the call itself - a call-inline-assign block,
or the target of a compound assignment.
A name in an element access names a parameter of the indexer, which the type
system takes from the getter; the accessor being called may name the same
parameters differently. C# cannot declare that, but other languages can, so
the names are read off the indexer. The names also have to stop where the
written arguments do, which is now done once where they are built rather than
again inside the ambiguity check.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
HandleAccessorCall had no way to express an omitted argument, so CallBuilder
asserted that none had been detected before it got there: any assembly
indexing through an indexer with an optional parameter hit that assert in a
Debug build, and Release wrote the defaults back out. An access now renders
through the same ArgumentList helpers as an ordinary call, and the steps that
disambiguate it can write the omitted arguments out again.
Two things had to reach that. The assigned value of a setter is the last
argument of the accessor call but not an argument of the access - the standard
adds it only for the invocation (12.6.2.1) - so it neither ends the run of
optional arguments nor is written out with them, and it is taken out of the
list before anything counts or casts the arguments. An indexer must also keep
one argument: dropping them all would turn the access into a property access,
which names a different member.
Readability names are turned off for an access. Rendering through
GetArgumentExpressions() would otherwise let them through, where master never
called it here, and `d[true]` would come out as `d[flag: true]` or, against an
override that renames the parameter, as `((Base)d)[flag: true]`.
Not covered: params indexers, [Optional] without a constant,
[DateTimeConstant]-style defaults, default(T) at a value-type instantiation,
and omitting a middle optional argument - the last of which plain calls do not
do either.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Cover the method body shapes from the PR review and remove the extra field-load guard so discarded conversions are kept as allocations without preserving the cache check.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
CachedDelegateInitializationWithField only fired when the instruction
after the caching `if` read the cache field exactly once. A discarded
conversion -- `_ = (Action<int>)M;` -- reads it zero times: Roslyn emits
the null check and the store, and nothing else. The transform bailed out,
so the `<>O` cache class and its `<0>__M` field survived into the output
under names no C# compiler will accept.
Zero usages is now handled: the `if` is replaced by the delegate
construction it guarded, which is what the source expressed, and the
caching disappears with the field. The rest of the method is checked for
loads of the same field first, since only then is this `if` the sole
initialization.
Closes#3965
A PEFile keeps its assembly mapped until it is disposed, and three fixtures
opened one and never did: DuplicateAssemblyReferenceTests both directly and
for every reference its resolver hands back, TypeForwarderResolutionTests for
everything the resolver probes, and LocationsInAstTests for the module behind
each decompiler. Their teardowns then deleted the directory those files live
in, which cannot work while the mapping is open, so a Windows run reported a
failed teardown for a run that otherwise passed. They now dispose what they
opened, the way WpfProjectExportTests already did.
Deleting is cleanup either way, so it goes through RepeatOnIOError rather than
straight at the file system, and that helper no longer rethrows: its last of
five attempts was deliberately unguarded, and four retries ten milliseconds
apart is a short window to wait for a handle that a virus scanner or the
indexer still holds. It now backs off to about a second and a half and, failing
that, reports and leaves the file behind, which costs nothing.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Whether a member reference has to name its target was decided in three places:
RequiresQualifier, and a copy each in the accessor and call paths. They
disagreed in two ways that both look like oversights.
Only the method group path failed to consult AlwaysQualifyMemberReferences.
That setting is on for the WinForms InitializeComponent method, which is also
decompiled with implicit method group conversion off, so every event handler
there came out as new EventHandler(Foo) while everything around it was
qualified.
A base target was the other, and the copies answered it three ways: never for
fields, always for accessors and method groups, and a virtual-only test for
calls. Dropping "base." leaves the reference dispatching virtually, which
reaches the same member unless the member can be overridden and the IL did not
dispatch virtually - so IsOverridable decides it, not the kind of reference.
Never is right only because a field access does not dispatch; always keeps
qualifiers that change nothing; and virtual-only is too narrow, since an
abstract or overriding member is dispatched virtually without carrying the
keyword. That last one drops the qualifier on base.OverridingMember, which
reaches this type's member instead of the base one the IL named, and the
escalation cannot catch it: with nothing overridden here the shorter spelling
binds to the same member and looks correct.
QualifierTests already covered base references, but only where the calling type
shadows or overrides the member, so the qualifier was always required and
over-removal could not show up. It now also covers a member overridden further
up the hierarchy. The spellings that change instead of round-tripping are
Ugly test cases.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
VB lowers error handling differently from C# and no fixture covered it:
every Catch brackets its body with SetProjectError/ClearProjectError,
Catch...When moves that call into the filter, and On Error becomes one
try block with a dispatch switch driven by line and handler state. The
expected output is the current decompilation, rough spots included
(delegate-invoked filter blocks; gotos into the dispatch switch that
leave unreachable code, which keeps VBOnError.cs out of the
warnings-as-errors project), so improvements show up as diffs. IL offset
labels differ per compiler and optimization level, hence the per-method
#if variants. Legacy vbc output was verified on Windows.
The correctness test covers what a pretty test cannot: catching object
made the decompiled On Error code fail to recompile with CS0155, and the
round trip shows the rewritten dispatch code still runs the same. Like
the mcs configurations, the legacy and Roslyn 2.10/.NET Core 2.2 ones
recompile the decompiled VB code with the latest Roslyn: C# 5 cannot
express the exception filters VB error handling compiles to, and the
.NET Core 2.2 Microsoft.VisualBasic.dll lacks Information.Err and
ProjectData.CreateProjectError.
Assisted-by: Claude:claude-opus-5:Claude Code
vbc builds an exception filter as a single non-short-circuiting
expression: `isinst`, then the user's `when` conditions, combined with
`and`. DetectCatchWhenConditionBlocks only knew the branch chain csc
emits, so the type test stayed inside the filter and the handler kept the
`object` variable it has in IL:
catch (object obj) when ((obj is Exception) & (num2 != 0) & (num == 0))
{
ProjectData.SetProjectError((Exception)obj);
A catch type has to derive from Exception, so that does not compile.
The conjunction form is matched too now, lifting the test to the catch
type as the block form already does. The other conjuncts stop running for
a non-matching exception once the test moves, so the filter has to be
pure for this to be invisible; PropagateExceptionVariable then drops the
castclass in the handler:
catch (Exception ex) when ((num2 != 0) & (num == 0))
{
ProjectData.SetProjectError(ex);
Closes#3659
The C# 5 compiler loads the awaiter into a fresh local before every
dynamic call site and before the ICriticalNotifyCompletion type test, and
the early non-aggressive inlining refuses to fold those copies. Every
await matcher identifies an await by the identity of its awaiter
variable, so each copy hid the awaiter from the matcher and the whole
await stayed undetected.
Each such copy has one store and one load, and after the dynamic call
sites are collapsed that load sits in the next instruction, which is the
case ILInlining.InlineOne already handles, including the check that the
source is not overwritten first. Folding is restricted to the IsCompleted
and GetResult call sites and the completion-interface type tests, so
dynamic calls in user code keep their locals: a blanket fold retypes
unrelated locals of the surrounding method (an enum local decompiled as
int plus a cast).
Two smaller divergences from the Roslyn shape sat behind that one: the
merge block of the AwaitOnCompleted/AwaitUnsafeOnCompleted diamond
clears doFinallyBodies before its leave, and the awaiter is restored
from its object-typed field with unbox.any rather than castclass.
The ILPretty fixture is the state machine from the issue's assembly with
its external types stubbed out, so the fix stays guarded where the legacy
compiler is unavailable. The Pretty fixture for dynamic await now runs
the pre-Roslyn configurations as well, which is the end-to-end guard on
Windows; its expected output records that the compiler copies an awaited
dynamic value into a local first and emits no debug name for it. The
dead stores an optimized build leaves before a try block containing an
await are not specific to dynamic - a plain await on a Task produces the
same three - so they sit behind an EXPECTED_OUTPUT-only block.
Checked against real legacy csc output (/o- and /o+): dynamic await in
plain code, loops, try/catch, try/finally and using.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
When an IfInstruction was created with resultType=bool, because only
values 0 or 1 are possible, then we need to preserve this property in
transforms -- otherwise the added correctness test would fail.
Declining every type-parameter operand was too broad. The reason a type
parameter has no lambda spelling is that `v == other` is CS0019 and boxing
both operands compares box identity where the tree compares values - and both
only apply while the parameter may be a value type. A parameter constrained to
a reference type compares as a reference, which is what the tree asks for and
what `t == null` spells, so it converts like any other reference comparison.
IsReferenceType is the distinction the type system already makes here:
TypeUtils.GetStackType maps a type parameter to Obj or VT by the same
question. An unconstrained parameter answers null and keeps declining.
Assisted-by: Claude:claude-opus-5:Claude Code