10 KiB
CLAUDE.md
Guidance for Claude Code (and future Claude sessions) when working on ILSpy.
What this codebase is
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. The ILSpy/ project IS the Avalonia UI — there is no separate "WPF version" in this tree.
Tech stack
- 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. Note that Simple's ComboBox chevron isToggleButton#toggle, notBorder#HighlightBackground. - Microsoft.Extensions.DependencyInjection + System.Composition MEF directly, with a small bridge. Do not use TomsToolbox.
- AvaloniaEdit theme must be registered in
Application.Stylesvia the StyleInclude orTextEditorrenders 0×0. - 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').
Project structure
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, ported to Avalonia (net10.0; part ofILSpy.Desktop.slnf). Not legacy.
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
Legacy / Windows-tied (porting backlog; do not modify unless explicitly asked to port them):
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); there is noILSpy.BamlDecompiler/project, only these tests
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 / ILSpy.VSExtensions.sln cover the legacy packaging.
Code conventions
- Don't reference "WPF" in source or comments. Describe what code does, not where it came from. "Mirrors WPF" comments rot once the legacy WPF tree is gone.
- Don't invent. Port what the previous app actually shipped, not every conceivable feature. Don't add Options-page settings that weren't there.
- 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.
Build / restore
- Common actions have repo-root pwsh scripts:
restore.ps1,build.ps1(-Configuration Debug|Release),clean.ps1,updatedeps.ps1,publish.ps1, andBuildTools/format.ps1. They targetILSpy.sln. - 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.
Commit workflow
- 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).
Test discipline
- 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. - After matcher / rewriter edits, run the relevant tests, not just the build.
dotnet buildgreen ≠ behaviour correct.
Framework-specific gotchas
- AvaloniaEdit hyperlinks. Flip
Editor.Options.RequireControlModifierForHyperlinkClick=falseonce on the editor; don't overrideConstructElementFromMatchper generator. - AvaloniaEdit hover.
OnQueryCursormust not sete.Handled=true.PointerHoverargs are captured at hover-start, not at fire-time. - AvaloniaEdit TextView coords.
GetPositionwants document-visual coords (addScrollOffset). RedrawIBackgroundRenderers viaTextView.InvalidateLayer. - Avalonia 12 clipboard.
IClipboard.SetTextAsyncis an extension method — needsusing Avalonia.Input.Platform;even when the namespace is already imported (ClipboardExtensionsis a separate static class). - ProDataGrid hierarchical cell layout. Use
Grid Auto/*inDataGridHierarchicalColumncell templates, notStackPanel. The first child gets clipped otherwise. - ProDataGrid two-way expanded sync. Bind
HierarchicalOptions.IsExpandedPropertyPathfor model↔grid sync; don't hand-rollNodeExpanded/PropertyChangedplumbing. - ProDataGrid row drag-drop. DataGrid-level
IsReadOnlyshort-circuits the drag controller. PutIsReadOnlyon the column instead. - Dock setup follows
samples/DockMvvmSample.App.axamlwiring, factory shape,DockControlmarkup,ContextLocator/DockableLocatorshould match the sample. Deviate only with a documented reason. - DockWorkspace tab routing. Every navigation reuses the active tab (replacing in place if the type changes). Only "Open in new tab" and "Freeze tab" carve out new tabs.
- Decompile-progress UI is an overlay. Never wipe
Text/Foldings/ etc. to show a spinner; keep editor state intact so cancel falls back to it. ILSpyTraceListenersurfacesDebug.Assertthrough the global dialog — keep it registered at startup.
Investigating dependencies
- Decompile NuGet packages with this repo's
ilspycmdto inspect dependency internals — don't grep binaries.
Onboarding new contributors
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.