* Fix anonymous-type lambda early-return emitting unresolvable cast
When a lambda's inferred return type contains an anonymous type and one
branch returns null, the decompiler emitted an explicit cast such as
`return (IEnumerable<<>f__AnonymousType0<int>>)null;`, which is invalid C#.
Skip the cast in IsPossibleLossOfTypeInformation for null literals whenever
the expected type contains an anonymous type:
null is implicitly convertible to any reference type, so no cast is needed,
and the anonymous type has no nameable form to cast to anyway.
Fixes#3751
ICSharpCode.Decompiler / ILSpyX / BamlDecompiler set RestoreLockedMode but
declared no RuntimeIdentifiers, so their lock files were RID-agnostic. A
RID-specific 'dotnet publish -r <rid>' of ILSpy (which declares all four
distribution RIDs) propagated the RID to these locked project references and
failed restore with NU1004 unless --no-restore or --force-evaluate was used.
Declare the same win-x64;win-arm64;linux-x64;osx-arm64 set on the three
libraries and regenerate the locks so a locked-mode publish works for every
platform out of the box.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Replace the Windows-only .bat helpers (clean / debugbuild / releasebuild /
restore / updatedeps and BuildTools/format) with cross-platform pwsh
scripts at the repo root: restore.ps1, build.ps1 (-Configuration), clean.ps1,
updatedeps.ps1 and BuildTools/format.ps1, alongside the existing publish.ps1.
Enable a packages.lock.json for every project by hoisting
RestorePackagesWithLockFile into the root Directory.Build.props (the four
core libraries set it individually before) and commit the generated locks,
so restores are repeatable and CI can cache packages off them.
Cache the NuGet packages folder in the three setup-dotnet workflows
(build-ilspy, build-frontends, codeql-analysis), keyed on the lock files
per the setup-dotnet caching guidance.
Scope the Debug "Verify package contents" check to the *.filelist outputs
it actually generates. A project's packages.lock.json is keyed only by
(framework, RID), with no host-OS axis, so a lock produced on Linux
legitimately differs from one produced on Windows whenever an OS-conditional
PackageReference applies (Debug+Windows pulls Microsoft.DiaSymReader*). The
Windows restore then rewrites those locks; that churn must not fail a step
whose job is to police the VSIX/MSI file lists.
Also drop the dead ILSpy.BamlDecompiler publish line from
publishlocaldev.ps1, mirroring the earlier publish.ps1 fix.
Assisted-by: Claude:claude-opus-4-8:Claude Code
XmlDocLoader's modern-.NET fallback built the ref-pack path from the EXACT runtime
patch version (shared/Microsoft.NETCore.App/10.0.8 -> packs/Microsoft.NETCore.App.Ref/
10.0.8/ref), but the targeting (ref) pack version usually differs from the installed
runtime patch -- e.g. pack 10.0.0 -- and a relocated CI install (DOTNET_INSTALL_DIR)
may carry only a different feature band. The exact-version lookup then missed and the
provider came back null, leaving CoreLib hovers undocumented. Broaden the search:
prefer an exact match, then the newest pack sharing the runtime's major.minor, then
the newest pack present.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The portable-PDB writer carried its knobs (no-logo, pdb id, progress,
progress title) as a growing list of optional WritePdb parameters.
Turn the type into a configured instance whose options are properties,
and add EmbedSourceFiles (default true): when a PDB is generated next
to a project export whose .cs are already on disk, embedding the source
again is redundant, so the caller can turn it off. The per-document
checksum/hash is computed either way, so documents still resolve.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DecompilerVersionInfo.template.cs is the source-of-truth for the
runtime version constants -- update-assemblyinfo.ps1 rewrites the
generated DecompilerVersionInfo.cs from it on every build, picking
up the live git revision and commit hash for the $INSERT...$ slots.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Ports the WPF assemblyList_CollectionChanged history-prune from
ILSpy/AssemblyTree/AssemblyTreeModel.cs to the Avalonia
DockWorkspace.OnAssemblyListChanged handler. NavigationHistory<T>
already exposed the RemoveAll(Predicate<T>) primitive; the caller
wiring was the missing piece. Without it, a tree-row click after
removing an assembly walked through NavigationEntry.DisplayText
on a stale TreeNodeEntry, which hit MemberReferenceTreeNode.Signature
-> Language.EntityToString -> ILAmbience.ConvertSymbol and NRE'd on
a now-null ParentModule.
Assisted-by: Claude:claude-opus-4-7:Claude Code
Promotes the modern .NET XML-doc lookup from the Avalonia port into the
shared ICSharpCode.Decompiler library so every host (WPF, Avalonia, any
third-party consumer of XmlDocLoader) gets hover/tooltip documentation
for system entities without per-host fallback wiring.
After the cluster-1/3/4 fixes converged every caller on the same matching
shape (match the slot/kind/type of a reference ILVariable, then check the
init value), the `Predicate<StLoc>` parameter was just a hole through which
each caller restated that logic verbatim. Fold the slot/kind/type check
into the helper and have callers pass just the reference variable and a
value matcher.
The multi-handler matcher only recognized a switch-instruction dispatch — but
when a try-catch has just two handlers (or a handful with non-consecutive K
values), Roslyn emits an if-chain instead:
if (num == K_1) br case_K_1; br nextBlock
; nextBlock { if (num == K_2) br case_K_2; <leave outer | br end> }
Add a parallel matcher that walks the if-chain and collects (K, case-block)
pairs the same way MatchSwitchDispatch does, plus the terminating leave/branch
as the default exit. Call it as a fallback when the switch matcher rejects.
Also clone the default-exit before re-adding it to the continuation block —
in the if-chain shape it's a child of a *different* block (a later step in
the chain), not the now-cleared switch instruction, so the in-place re-add
relied on the switch's release cascade and didn't generalize.
Closes Cluster 2 from #3745.
The flag-based early-return rewriter was tied to one specific lowered shape:
the try body's flag-setter had to be exactly `stloc flag(K); leave try`, the
post-try check had to be a `br checkBlock` (not an inline `IfInstruction`), and
the early path had to be a direct Leave or a forward to a one-instruction
leave-block whose target was the function body. None of those hold for
`try { try { return X; } finally { await ... } } finally { await ... }`:
- The inner flag-setter has a leading capture-forwarding store
(`stloc capture(X); stloc innerFlag(K); leave inner-try`).
- The inner check-block's early path branches to a multi-instruction helper
that sets the *outer* flag and leaves the outer try, instead of being a
direct return.
- SplitVariables hands out a separate ILVariable for the pre-init flag store
when the in-handler store is in a disjoint dataflow region.
Rebuild the matcher around the idea of a "template" — the chain of stores
the early path performs before its terminating Leave. Each flag-setter then
becomes its own prefix stores + a clone of the template, which collapses the
inner-then-outer flag chain in two passes (inner first, outer second, because
descendant order visits the inner TryFinally first). Also extend the
flag-setter scan to walk the whole try-block's descendants — after the inner
rewrite, the inner's spliced flag-setter lives inside the inner-try container
but still leaves outwards to the outer try, so it's an outer flag-setter from
the outer's perspective.
Add a `RUNTIMEASYNC` preprocessor symbol (defined when `EnableRuntimeAsync`
is set) and gate the new return-from-try-finally fixtures on it — the
state-machine async pipeline doesn't recover this shape, so it would expand
the same source into the `int result; try { ...; result = X; } finally { ... }
return result;` verbose form and the Async (state-machine) pretty test would
regress.
Closes Cluster 1 (1.1, 1.3) from #3745. Cluster 1.2 (void `return;` at the
end of a try-finally body) and 1.4 (break/continue across a try-finally) are
left for a follow-up: both round-trip semantically equivalently but the AST
emitter drops a trailing void `return;` and the break/continue lowering uses
a switch dispatch that the current single-K matcher can't recognize.
`try { throw new ...(); } finally { await ... }` lowers to a try whose only
exit is the throw (handled by the synthetic catch). The existing matcher
required at least one outward Branch to the continuation, which is too strict
— a throw-only try body produces zero outward branches but is still a valid
lowered shape. Two follow-on fixes were also needed:
- The pre-init's ILVariable diverges from the in-handler store after
SplitVariables when the try body has no path that reaches the dispatch's
load without going through the catch; match the flag init by slot/kind/type
instead of identity (same workaround the multi-handler matcher uses).
- With a throw-only try body the new TryFinally has unreachable endpoint,
so appending the no-exception successor after it would put a non-final
unreachable-endpoint instruction in the parent block. Skip the append in
that case — the parent block's endpoint is already correctly unreachable.
Closes Cluster 4 from #3745.
The single-handler try-catch matcher was tied to the top-level shape: it
required the try-catch be the last instruction in its parent block, that the
post-catch "no exception" path be a direct Leave that exits the function, and
that the flag-init's ILVariable be identical to the in-handler flag store.
None of those hold for an inner try-catch sitting inside an outer try-finally
where both await — the inner is followed by a `br continuation`, the no-exception
path leaves the outer try-block (not the function), and SplitVariables hands
out a separate ILVariable for the pre-init store.
Drop the "must be last instruction" gate, accept Leave-to-any-ancestor and
cross-container Branch as the no-exception exit (extracted into a new
`IsContainerExit` helper), and match the flag-init by slot/kind/type the same
way the multi-handler matcher already does.
Closes Cluster 3 from #3745.
When a return crosses an enclosing try-finally with await, runtime-async lowers it as: capture the return value, set an int flag to a unique non-zero value, leave the try block normally so the finally runs, then post-finally check "if (flag == K) return capture;". Detect that pattern after my outer try-finally rewrite (or, in optimized builds, the compiler-emitted TryFinally directly) and replace each capture-flag-and-leave site with a direct "leave outer (capture)" — the leave still passes through the TryFinally, so the user's finally body executes before the function returns, which matches the source-level semantics.
Handles both the "if (flag == K)" and "if (flag != K)" check forms (the optimizer emits the latter). Closes the last gap in Issue2436 — RuntimeAsync now passes both Optimize and non-Optimize modes; the full RuntimeAsync* sweep is 12/12 green.
Also remap reads of the captured-obj local inside the cleaned filter so optimized builds (where Roslyn inlines the typed-cast directly into the user filter expression instead of stashing it in a local) render against the catch variable rather than against "((T)obj)".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For an async method on a value type Roslyn cannot keep a managed reference to the caller's struct alive across an await, so it copies *this into a local at method entry and rewrites every "this.field" access to go through the copy. The decompiler then sees an extra "AsyncInStruct asyncInStruct = *this;" prelude and renders user-level "i++" as "asyncInStruct.i++". State-machine async normally avoids this because TranslateFieldsToLocalAccess already remaps the captured-this field back to the function's own this parameter.
Detect the prelude in runtime-async methods (entry-point stloc V_X(ldobj T(ldloc this)) with the local typed as the containing value type) and rewrite every "ldloc V_X" / "ldloca V_X" to go through the function's this parameter instead, then drop the now-dead copy. The mutation semantics are unchanged — runtime-async struct methods never reflect mutations back to the caller anyway, so re-pointing the access at this is purely a fidelity restoration.
Brings AsyncInStruct.Test back to its source ("i++" / "i + xx.i"). The only remaining failure in RuntimeAsync is Issue2436 (early-return-from-nested-catch encoded as a flag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn's runtime-async lowering uses AsyncHelpers.Await(Task) for Task awaitables (already handled by TransformAsyncHelpersAwaitToAwait in EarlyExpressionTransforms) but emits a manual GetAwaiter / get_IsCompleted / AsyncHelpers.UnsafeAwaitAwaiter / GetResult sequence for non-Task awaitables — YieldAwaitable, ConfiguredCancelableAsyncEnumerable.Enumerator from await foreach, etc. Add a new RuntimeAsyncManualAwaitTransform invoked from AsyncAwaitDecompiler's runtime-async dispatch that recognizes the three-block shape (head with stloc awaiter + IsCompleted check + branch, pause block calling UnsafeAwaitAwaiter, completed block starting with GetResult), strips the suspend machinery, and replaces the GetResult call with an Await IL instruction. When GetAwaiter takes the address of a temporary set in the same block, also drop the temporary store and use the underlying awaitable expression.
This collapses the LoadsToCatch await-Task.Yield bodies. AsyncForeach should benefit too (its MoveNextAsync awaits go through this path).
When the user writes multiple catch clauses on a single try, runtime-async lowers each catch's body to "[stloc tmp(ex);] [stloc obj(...);] stloc num(K_i); br continuation" with a unique K_i per handler, and the post-catch flow becomes a switch dispatch on `num` that branches to each user-level catch body. Add a TryRewriteMultiHandlerTryCatch driver that mirrors the single-handler match (using NormalizeRuntimeAsyncFilter for filter cleanup), recognizes the post-catch SwitchInstruction, and uses the existing dominator-based block move to relocate each switch case into the corresponding handler body, remapping that handler's per-handler synthesized variables (and the shared filter obj) back to the catch variable.
The shared obj local can no longer be remapped function-wide during filter normalization — that would tag every dispatch idiom with whichever handler ran first — so record the obj per handler in a dictionary and let TryRewriteTryCatch / TryRewriteMultiHandlerTryCatch remap it scoped to each moved catch body. The pre-init "stloc num(0)" is matched by slot index rather than ILVariable identity, since SplitVariables splits the dead pre-init off from the in-handler stores.
Resolves the LoadsToCatch case. Filter normalization extends to the typeless `catch when (filter)` form (isinst Object in the filter), recovered as `catch when` in the AST output. Remaining failures in RuntimeAsync are now multi-await expressions, async-in-struct, and a couple of unrelated decompilation issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn lowers `catch (T ex) when (filter)` to `catch object when (BlockContainer { isinst T; obj=ex; <filter> })` even when T is `object` (the source-level `catch when (filter)` form). Run a pre-pass over every catch handler that matches the four-block diamond (entry isinst-gate, trueBody with obj-store + user filter, falseBody constant-false, merge leave-with-result), strip the obj-store machinery, retype the handler variable when T is a more specific type than object, and remap reads of the synthesized obj/tmp/typedEx variables back to the handler variable. After that the catch body is the same simple flag-store shape that TryRewriteTryCatch already handles, so the existing match runs unchanged.
Resolves the RethrowDeclaredWithFilter and ComplexCatchBlockWithFilter cases. Multi-handler catches (LoadsToCatch) still fail because they use a multi-valued discriminator that isn't reduced yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn's runtime-async lowering flattens these into a TryCatch[object] with a captured-rethrow pattern (try-finally) or a TryCatch[T] with a flag-int discriminator and a guarded post-catch body (try-catch). Add a new transform invoked from AsyncAwaitDecompiler when the state-machine matches fail and the method has the runtime-async impl bit; it pattern-matches both shapes and rewrites them back to TryFinally / TryCatch with the original catch body inlined into the handler.
The state-machine and runtime-async lowerings of try-finally use the same catch-handler shape and the same dominator-based finally-body extraction, so promote those to internal static helpers (MatchObjectStoreCatchHandler, MoveDominatedBlocksToContainer) on AwaitInFinallyTransform and call them from the new transform. Filter-bearing catches and multi-handler tries are still left to the standard pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert `call System.Runtime.CompilerServices.AsyncHelpers.Await(value)`
to the IL Await instruction whenever DecompilerSettings.RuntimeAsync is
enabled. The state-machine async pipeline (AsyncAwaitDecompiler) already
produces the IL Await for downstream transforms (UsingTransform's
MatchDisposeBlock pattern-matches on it via UnwrapAwait); doing the
conversion in EarlyExpressionTransforms gives the runtime-async output
the same canonical shape before any consumer runs.
Add TaskType.UnpackAnyTask and use it in ILReader.Init / ReadIL so methodReturnStackType and function.AsyncReturnType agree for runtime-async methods that return ValueTask/ValueTask<T> or any [AsyncMethodBuilder]-attributed custom task type. Previously only Task/Task<T> were unpacked, leaving AsyncReturnType=void while the IL Leave value carried the unpacked element type, which tripped the StackType assert in ExpressionBuilder.Translate.
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.
ResXResourceWriter is a verbatim port of the Mono implementation
(see file header). Both warnings flag deliberate decisions in the
upstream port that we preserve for fidelity:
* CA1063 — Dispose() is virtual and the protected Dispose(bool) is
not, the inverse of the canonical pattern; keeping the Mono shape.
* CA2213 — the writer's stream / textwriter fields are caller-owned
and intentionally not disposed by the writer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The C# 9 IntPtr / UIntPtr guard in IsBinaryCompatibleWithType read
type.Kind is not TypeKind.NInt or TypeKind.NUInt
which parses as `(is not NInt) or (is NUInt)` — true unless
Kind == NInt. The intent (per the surrounding comment "but not nint
or C# 11 IntPtr") is "Kind is neither NInt nor NUInt", which needs
parentheses around the alternation:
type.Kind is not (TypeKind.NInt or TypeKind.NUInt)
Effect: when Kind == NUInt the branch no longer mistakenly applies
the C# 9 IntPtr-only restrictions (suppressing compound assignment
without nint, disallowing shifts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ExtensionDeclaration.SymbolKind (CA1065) — was throwing
NotImplementedException; return SymbolKind.TypeDefinition to match
TypeDeclaration / DelegateDeclaration, since `extension` declarations
are type-level.
* CustomAttribute.DecodeValue (CA2002) — replace `lock(this)` on the
sealed-but-internal class with a private syncRoot field.
* PlainTextOutput (CA1001) — implement IDisposable; track an
ownsWriter flag so we only dispose the underlying TextWriter when
the parameterless constructor created its own StringWriter.
* DotNetCorePathFinder (CA1060) — move the libc realpath / free
PInvokes into a private nested NativeMethods class.
* ILSpy.ReadyToRun (CA1016) — add [assembly: AssemblyVersion("1.0.0.0")]
in Properties/AssemblyInfo.cs to match the BamlDecompiler plugin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four cases where the analyzer rule conflicts with intentional design:
* EmptyList<T>.IDisposable.Dispose (CA1063) — explicit IDisposable on
IEnumerator<T>; making it public would conflict with the rest of the
IList<T> / IEnumerator<T> surface.
* MetadataFile.SectionHeaders (CA1065) — throw documents that this
MetadataFileKind has no PE sections; PE-like derived kinds override.
* LongSet.GetHashCode + LongSet itself (CA1065 + CA2231) — explicit
guards against using LongSet in hash containers / via equality
operators; SetEquals is the supported comparison and
IEquatable<LongSet>.Equals is itself [Obsolete].
* AnnotationList.Clone (CA2002) — AnnotationList is a private nested
type; the surrounding Annotatable class deliberately locks on the
AnnotationList instance to serialize annotation reads/writes, and
external code cannot obtain a reference to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MetadataFile now declares IDisposable using the canonical pattern
(public non-virtual Dispose() + protected virtual Dispose(bool)).
PEFile and WebCilFile become sealed and override Dispose(bool) to
release the PEReader and MemoryMappedViewAccessor they own;
ResourcesFile is also sealed. PortableDebugInfoProvider disposes the
MetadataReaderProvider it owns. LoadedAssembly implements IDisposable
and disposes both the loaded MetadataFile and the debug-info provider.
AssemblyList.Unload / Clear / ReloadAssembly / HotReplaceAssembly now
dispose the LoadedAssembly instances they evict, fixing a resource leak
where every "Reload Assembly" held the previous PEReader (and the
underlying file handle / memory-mapped view) alive until GC eventually
finalized it.
The disposal contract terminates at the AssemblyList tier: downstream
holders of MetadataFile (MetadataModule, DecompilerTypeSystem,
AssemblyListSnapshot, ...) hold borrowed references rather than owned
ones, so making the base IDisposable does not cascade into CA1001 /
CA2213 warnings elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>