The project listed all 576 source files as explicit <Compile Include> with
EnableDefaultItems=false, so every new file had to be added by hand. Switch to
the SDK's default **/*.cs glob and keep only the exclusions that are actually
needed:
- Properties/DecompilerVersionInfo.cs is generated before build and absent when
the glob is evaluated on a clean checkout, so it is still included explicitly
(with a paired Remove to avoid a duplicate once a prior build produced it).
- DecompilerVersionInfo.template.cs is a placeholder template, never compiled.
Also delete the hand-written DecompilerAstNodeAttribute.cs: the source generator
emits this attribute (RegisterPostInitializationOutput), so the on-disk copy was
stale dead code -- never compiled, and parameterless where real usages pass
[DecompilerAstNode(hasPatternPlaceholder: true)]. Removing it drops what would
otherwise be a third exclusion.
Default None globbing stays off so the explicit None entries remain the
authoritative list. The evaluated Compile set is identical to the previous 576
files; verified by a clean build with the version file both present and absent.
Assisted-by: Claude:claude-opus-4-8:Claude Code
After the Role and TokenRole hierarchies were removed, the Roles class no
longer held any roles -- only the punctuation and keyword text the output
visitor writes (LPar, Arrow, ClassKeyword, ...). The name was a misleading
vestige of the deleted system. Rename it to Tokens, which is what it now
is; the constants and their values are unchanged.
Also refresh the CSharpSlotInfo doc, which still described slot identity as
the successor to the removed node.Role == Roles.X comparison.
Assisted-by: Claude:claude-opus-4-8:Claude Code
TokenRole was a printer-side descriptor whose only jobs were holding a token's
text and giving the writers an identity to single out specific tokens. The text
becomes plain const strings on the nodes, and the few identity checks are
reexpressed as node-stack context: interpolation braces are recognized by an
Interpolation on the writer's stack, record class versus struct coloring keys
off TypeDeclaration.ClassType, and the accessor/this/base/override cases fall
out of the surrounding node. WriteKeyword/WriteToken drop the descriptor
parameter. The constants are named for what the token is: a keyword, a symbol
token, or a modifier.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Slot identity already lived in node.Slot/CSharpSlotInfo/SlotKind after
the storage flip, so the parallel Role/Role<T> child model was redundant.
The Role-keyed mutation/query API is reexpressed over SlotKind, and the
role-index packing on AstNode flags is gone.
Because a node's Slot is now derived from its index in its parent rather
than stored on the child, the located-AST reattach in
InsertMissingTokensDecorator must capture the child's slot kind before
Remove() detaches it.
Assisted-by: Claude:claude-opus-4-8:Claude Code
NodeType was NRefactory's coarse node category, but only three reads remained
here: two checks now expressed as "is not Trivia" and one debug assert for the
pattern category. Remove the enum, the abstract property, every per-node
override, and the generator's emission, preserving the pattern-placeholder case
through an IPatternPlaceholder marker interface the output-visitor assert
checks. Also remove the unused PrimitiveExpression.AdvanceLocation helper.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Introduce the successor to node.Role for child-slot identity: the generator
emits a CSharpSlotInfo per [Slot], exposed as node.Slot, plus a shared SlotKind
enum for the polymorphic "is this node in an embedded-statement / condition /
base-type slot?" comparisons a per-node identity cannot express. Migrate the
printer and transform position checks from node.Role to node.Slot and
node.Slot.Kind, and read identifier children and role-keyed writes through the
typed properties. Role is still present and is removed later; output is
unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Comments and preprocessor directives were positional children interleaved
into the child list, and punctuation, keywords and operators were token-node
children. Add a leading/trailing trivia side-channel for comments and
directives, emit it from the output visitor, and re-home every comment
receiver onto it (including inside-block comments as comment-only empty
statements and undecodable attribute arguments as an ErrorExpression). With
locations and sequence points no longer sourced from token nodes, stop
reconstructing them on the locations path and delete CSharpTokenNode,
CSharpModifierToken and InsertSpecialsDecorator. The AST no longer carries
token children or positional comments; output is byte-identical.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Introduce a Roslyn source generator that emits the visitor boilerplate for
the C# AST from [DecompilerAstNode]-tagged node declarations: the
IAstVisitor interface, the AcceptVisitor overloads, the pattern-placeholder
nodes, and the initial DoMatch support. AccessorKind lets an accessor's
keyword be chosen independently of its role, an early step toward shedding
the NRefactory role model.
On FIPS-mode systems the platform crypto provider refuses to create
SHA-1 instances (OpenSSL: error:03000098 invalid digest), so merely
displaying a strong-named assembly's identity failed. The public-key
token is a non-secret identity hash whose algorithm is fixed by
ECMA-335, so the two token sites now use dotnet/runtime's managed
Sha1ForNonSecretPurposes, vendored with its license header intact and
shielded from the repo formatter via generated_code in .editorconfig
so future upstream syncs diff cleanly. IncrementalHash was considered
and rejected: like SHA1.Create(), it resolves the digest through the
host crypto policy, and Roslyn's equivalent token code also relies on
the platform SHA-1, so it offers no precedent for FIPS safety.
Assisted-by: Claude:claude-fable-5:Claude Code
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
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).
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>
This way we avoid having to extract later, as we will never inline if the `isinst` argument if this could result in it being unrepresentable in C#.
This commit also refactors inlining restrictions to avoid requiring special cases in ILInlining itself.
But when making this change, I discovered that this broke our pattern-matching tests, and that the weird IL with double `isinst` is indeed generated by the C# compiler for `if (genericParam is StringComparison.Ordinal)` style code. So instead we also allow `isinst` with a `box(expr-without-side-effects)` argument to be represented with the `expr is T ? (T)expr : null` emulation.
Add explicit System.Security.Cryptography.Pkcs dependency to avoid security vulnerability warning in ILSpyCmd.
Suppress security vulnerability warnings in test projects.