Guidance for Claude Code (and future Claude sessions) when working on ILSpy.
ILSpy is a cross-platform .NET assembly browser / decompiler built on Avalonia 12, on top of the cross-platform ICSharpCode.ILSpyX and ICSharpCode.Decompiler core libraries.
- Avalonia 12 (not 11.x).
- AvaloniaEdit for the decompiled-code text view.
- Dock (wieslawsoltes/Dock) for the panel layout. NuGet id ≠ CLR namespace —
Dock.Controls.Recyclinglives inAvalonia.Controls.Recycling; decompile before guessing xmlns. - Avalonia.Xaml.Behaviors for attached-behaviour glue.
- Avalonia.ExtendedToolkit (mameolan) for controls not in Avalonia core.
- Simple theme (not Fluent). Check
App.axaml/ csproj before assuming Fluent. - Microsoft.Extensions.DependencyInjection + System.Composition MEF directly, with a small bridge.
- Central package management is enabled — every
PackageReferenceneeds a matchingPackageVersioninDirectory.Packages.props. - Target framework:
net10.0(cross-platform) for the main app. The test projects targetnet11.0(andnet11.0-windowsfor tests that intentionally exercise Windows-only behaviour) so they run on the runtime thenet11.0preview build SDK ships, without installing a separatenet10.0runtime in CI.TestPluginstaysnet10.0(it is loaded as a library by the net11 test host, so its TFM need not track the test projects').
Avalonia UI:
ILSpy/— the Avalonia UI appILSpy.Tests/— headless Avalonia UI tests (Avalonia.Headless.NUnit)ILSpy.Tests.Windows/— Windows-only UI tests (OS-gated;net11.0-windows)ILSpy.ReadyToRun/— ReadyToRun-viewer plugin.
Cross-platform core (decompiler engine + shared support):
ICSharpCode.Decompiler/— core decompiler library (multi-targeted, cross-platform)ICSharpCode.ILSpyX/— shared UI-host-agnostic support libraryICSharpCode.BamlDecompiler/— BAML parsing libraryICSharpCode.ILSpyCmd/— CLI front-end (ilspycmd)ICSharpCode.Decompiler.Tests/+ICSharpCode.Decompiler.TestRunner/— decompiler test suite and its out-of-process runnerICSharpCode.Decompiler.PowerShell/— PowerShell cmdlets (netstandard2.0)
Test support:
TestPlugin/— sample plugin exercising the plugin-loading system (net10.0)TestFixtures.Resources/— generates resource fixtures consumed by the decompiler tests
Windows-only frontends, packaging, and tests:
ILSpy.AddIn/,ILSpy.AddIn.VS2022/— Visual Studio add-ins (net472)ILSpy.Installer/— WiX installer (net472)ILSpy.BamlDecompiler.Tests/— BAML-decompiler tests, still WPF/Windows-bound (net11.0-windows)
Solutions & filters: ILSpy.sln builds everything; ILSpy.XPlat.slnf is the decompiler libs + ilspycmd + their tests (no UI — the Linux CI target); ILSpy.Desktop.slnf is the UI plus its dependencies and tests; ILSpy.Installer.sln covers the legacy packaging, and ILSpy.VSExtensions.slnx the VS 2022 extension (dotnet build-able).
ILSpy-tests/is a git submodule (https://github.com/icsharpcode/ILSpy-tests, branchmaster) holding large real-world assemblies and pre-built fixtures used by the heavyweight decompiler tests — the round-trip suite (ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs) and a few IL-pretty cases (e.g.FSharp/FSharp.Core.dll).- It is not checked out by default, because it is large. Tests that need it call
Assert.Ignorewhen the directory is absent (seeRoundtripAssembly/ILPrettyTestRunner), so the rest of the suite runs without it — a green local run does not mean the round-trip tests ran. To run them, populate it first:git submodule update --init ILSpy-tests(or clone it separately to that path). - It opts out of the host build settings on purpose. The submodule ships its own
ILSpy-tests/Directory.Build.propsthat setsTreatWarningsAsErrors=false; its mere presence also stops MSBuild's upwardDirectory.Build.propssearch at the submodule root, so the repo-wide warnings-as-errors (and other root build properties) don't leak into the fixtures, which intentionally contain warning-generating code. Don't delete that file or "fix" warnings inside the fixtures. - Bumping it is a normal submodule pointer update: check out the desired commit inside
ILSpy-tests/, then commit the changed submodule gitlink in the host repo (subject like "Bump ILSpy-tests: ...").
- Comments must stand on their own, with no memory of how the code was written. A comment must make sense to someone reading the file cold -- with no knowledge of the chat, PR, commit, or agent session that produced it. Describe the code as it is now; never reference "the change", "the previous version", "the old approach", "as requested", "we discussed", "now we", a step that was removed, or anything else that only means something inside the conversation that wrote it. If you can't explain it without that context, it isn't a code comment.
- Never undo edits you didn't make this session. If a file carries modifications made outside the session -- by a human, or surfaced by the harness as "modified outside the session / by the user or a linter" -- treat them as intentional. Do not revert, overwrite, "clean up", or discard them, even when they look unrelated, wrong, or in the way of your change, without explicit confirmation first. Work around them or ask; never silently drop someone else's work.
- TDD for new features. Write the failing test, show it red, implement, show it green. Never skip the red step.
- Strict pattern matchers. Decompilation pattern matchers should default to
∀over the structurally guaranteed shape — loosen only when a legitimate fixture is rejected (capture it as a regression test first). - No silent returns in tests. When an expected component is missing, assert and fail. Never
returnearly to bypass the assertion. - ASCII-only in code and comments. Do not use non-ASCII characters (em-dash, smart quotes, arrows, math symbols, etc.) unless genuinely necessary for what the code expresses.
- en-US English in all source code, comments, identifiers, and log/error strings.
- Use the repo-root pwsh scripts for these tasks, not raw
dotnetcommands -- they carry the flags that keep the repo consistent and all targetILSpy.sln(extra args are forwarded):- restore ->
restore.ps1 - build ->
build.ps1(-Configuration Debug|Release) - update deps / regenerate lock files ->
updatedeps.ps1 - format ->
BuildTools/format.ps1 - clean ->
clean.ps1; publish ->publish.ps1
- restore ->
- Why this matters:
restore.ps1andupdatedeps.ps1pass-p:RestoreEnablePackagePruning=false, so they keep everypackages.lock.jsonwhole. A baredotnet restore/dotnet build(which restores implicitly) prunes the lock files -- silently stripping the transitive/all-RID/DiaSymReader entries the repo deliberately carries -- and surfaces as a spuriouspackages.lock.jsondiff. Restore withrestore.ps1, then build withbuild.ps1 --no-restore; if a bare build ever leaves a prunedpackages.lock.jsonmodified, discard it (git checkout -- <path>). - Every project generates a
packages.lock.json(RestorePackagesWithLockFileis set in the rootDirectory.Build.props); CI NuGet caching keys off these. After adding or bumping aPackageReference/PackageVersion, regenerate the lock files withupdatedeps.ps1(i.e.dotnet restore ILSpy.sln --force-evaluate -p:RestoreEnablePackagePruning=false) and commit them. The core libraries additionally setRestoreLockedMode, so a plain restore there fails until the lock file is refreshed. - The pre-commit hook runs
dotnet formaton the whole solution -- it IS the formatter. Always let the hook run; never commit.cswith--no-verify. Bypassing it lands unformatted code and forces history-wide reformat rebases later.--no-verifyis acceptable only for commits that touch no.cs(e.g..yml/.md-only). - Line endings are a Windows-only concern. The repo is
* text=auto, so blobs are stored LF and git renormalizes on commit. On Windows, save new files CRLF so the hook'sgit add -udoesn't churn EOLs. On Linux/macOS, leave new files LF and do notunix2dos-- forcing CRLF there makes the whole working tree show as phantom-modified ingit status(and is normalized back to LF on commit anyway). - Partial commits need stash-and-pop. The format hook only auto-formats when the staged set equals the working tree. Stash unstaged remainder before committing a subset.
- Subject is a succinct phrase describing the change. Target <= 72 chars. No area prefix; the subject itself should make the change clear.
- Body explains the why and the non-diff context only: the constraint, the prior incident, the decision, what was tried and rejected, the invariant that motivated the change. Keep it short — one short paragraph is usually enough. The diff already shows the what — don't restate it, and don't enumerate per-file changes.
Fix #NNNN: ...closes an issue.#NNNNreferences one without closing.- Small follow-up fixes against a commit still on the branch get squashed back via
git commit --fixup+git rebase --autosquash, not appended as "fix X" commits. - en-US English in subject and body. ASCII-only unless a non-ASCII character is genuinely required for what the message describes.
- AI attribution: use
Assisted-by:, notCo-Authored-By:. Following the Linux kernel's coding-assistants guidance (https://docs.kernel.org/process/coding-assistants.html#attribution), an AI-assisted commit ends with a trailer of the formAssisted-by: AGENT_NAME:MODEL_VERSION:HARNESS— agent, model id, and the harness that ran it, colon-separated, e.g.Assisted-by: Claude:claude-opus-4-8:Claude Code. Use the session's actual model id and harness. Don't list analysis/build tools. Do not add aCo-Authored-By:line for the AI, and an AI agent must not add aSigned-off-by:(only a human can certify the DCO).
- Always run the test suite with
--report-trxso failures survive:dotnet test --solution ILSpy.sln --report-trx(the repo pins Microsoft.Testing.Platform inglobal.json; the baredotnet test <sln>form is the old VSTest syntax). Don't dismiss failures as flaky without first reproducing in isolation, then running repeatedly. - The decompiler test suite (test kinds, fixture structure, how to write tests, the compiler-matrix model) is documented in ICSharpCode.Decompiler.Tests/CLAUDE.md.
- After matcher / rewriter edits, run the relevant tests, not just the build.
dotnet buildgreen ≠ behaviour correct.
- Decompile NuGet packages with this repo's
ilspycmdto inspect dependency internals — don't grep binaries.
If you're picking up this codebase fresh, read Program.cs → App.axaml.cs → Views/MainWindow.axaml.cs to trace startup, then AssemblyTree/AssemblyListPane.axaml.cs and TextView/DecompilerTextView.axaml.cs for the two main panes. The MEF composition graph in AppEnv/AppComposition.cs wires the rest.