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
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
Decompiler.Tests targeted net11.0-windows only because
Microsoft.DiaSymReader.Native is Win-only and Tester.cs hardcoded ilasm.exe
/ ildasm.exe filenames. Both are narrow problems: gate the Native package
to Windows, replace the ".exe" suffix with an OS-aware helper, and drop
the -windows TFM. The Microsoft.NETCore.IL{,D}Asm metapackages' RID graph
already ships ilasm/ildasm for linux + osx via per-RID runtime sub-packages
when RuntimeIdentifier resolves dynamically, so the tool just lands in the
test output dir with the right suffix on every host.
Windows-only test fixtures (RoundtripAssembly + the RunWithTestRunner,
SignAssembly, FindMSBuild call sites) gate via Assert.Ignore +
[Platform("Win")] so they skip on non-Windows instead of failing. CA1416
joins NoWarn -- the analyzer can't see the runtime guards, and the test
inputs (Console.CapsLock, Registry access) are decompilation targets, not
infrastructure.
BuildTools/pre-commit learns to find dotnet-format on non-Windows (XDG
basedir + no .exe suffix) so the hook can run wherever the build now does.
Assisted-by: Claude:claude-opus-4-7:Claude Code
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.
Two methods exercise `Task<int>.ConfigureAwait(bool)`: a single false-flag form
and a mixed false/true form that combines two awaits in a return expression.
Both cases run through the regular state-machine and runtime-async pipelines
(RuntimeAsync reuses Async.cs as its source).
Gated by `#if ROSLYN2` because Roslyn 2+ preserves named-argument metadata at
the call site, so the decompiler renders `continueOnCapturedContext: false`
when the binary was compiled by Roslyn 2+ and positional `false` for default
csc / Roslyn 1.3.2.
Also adds NoInliningTaskMethod — an async method carrying
[MethodImpl(MethodImplOptions.NoInlining)] — so the runtime-async path
exercises the impl-attribute masking added in the scaffolding commit:
MetadataMethod strips the synthesized MethodImplOptions.Async (0x2000) bit
from the decompiled output, and unrelated impl bits like NoInlining (0x0008)
must still render in the surfaced [MethodImpl(...)] attribute.
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.
* .NET 11 RC2 minimal changes
* Heuristic for transport feed Roslyn selection
* Microsoft.CodeAnalysis.NetAnalyzers from main NuGet feed
* Use the VS2026 image
* Switch all test projects to net11
* Extract constants
* Include vsix with plain nuget.config files
This improves how function pointers are decompiled.
* ExpressionBuilder::VisitLdFtn now properly constructs the calling conventions.
* FunctionPointerType::FromSignature now checks whether a modopt type affects the calling convention.
Various improvements regarding primary constructor decompilation, including:
- introduce `HasPrimaryConstructor` property in the AST, as there is a difference between no primary constructor and a parameterless primary constructor
- improved support for inherited records and forwarded ctor calls
- exclude non-public fields and properties in IsPrintedMember
- introduce an option to always make the decompiler emit primary constructors, when possible