RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Diff/icsharpcode-ilspy-icsharpcode-decompiler-tests-claude ↔ icsharpcode-ilspy-claude

Comparison

A · CLAUDE.md · icsharpcode/ILSpyB · CLAUDE.md · icsharpcode/ILSpy
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections07110%
Commands01130%
Section tags40544%

What each file covers

Sections

0 shared · 7 only in A · 11 only in B
  • − ICSharpCode.Decompiler.Tests guide
  • − The matrix-testing model
  • − Test kinds
  • − How to add a test
  • − Conditional expectations (#if) and comparison rules
  • − Probing compiler codegen
  • − Running
  • + CLAUDE.md
  • + What this codebase is
  • + Tech stack
  • + Project structure
  • + ILSpy-tests submodule
  • + Code conventions
  • + Build / restore
  • + Commit workflow
  • + Test discipline
  • + Investigating dependencies
  • + Onboarding new contributors

Commands

0 shared · 1 only in A · 13 only in B
  • − dotnet test --solution ILSpy.sln --report-trx --filter FullyQualifiedName~PrettyTestRunner.SwitchExpressions
  • + dotnet build
  • + git submodule update --init ILSpy-tests
  • + dotnet
  • + dotnet restore
  • + git checkout -- <path>
  • + dotnet restore ILSpy.sln --force-evaluate -p:RestoreEnablePackagePruning=false
  • + dotnet format
  • + git add -u
  • + git status
  • + git commit --fixup
  • + git rebase --autosquash
  • + dotnet test --solution ILSpy.sln --report-trx
  • + dotnet test <sln>

Section tags

4 shared · 0 only in A · 5 only in B
  • + code-style
  • + architecture
  • + git-pr
  • + dependencies
  • + agent-behaviour
  •   build
  •   test
  •   testing-strategy
  •   do-not

Line diff

+82 added−104 removed20 unchanged16.1% identical
icsharpcode/ILSpy · ICSharpCode.Decompiler.Tests/CLAUDE.md
@@ −1 @@
1# ICSharpCode.Decompiler.Tests guide
2 
3How the decompiler test suite is structured, what each test kind does, and how to add tests.
4 
5## The matrix-testing model
6 
7Most fixtures run one logical test against a whole matrix of compilers and options: an NUnit
8`[Test]` method takes a `CompilerOptions` parameter fed by `[ValueSource]` from static config
9arrays declared per runner (`defaultOptions` including mcs, `roslynOnlyOptions`,
10`roslyn2OrNewerOptions`, `roslyn3OrNewerOptions`, `roslyn4OrNewerOptions`, each also in a
11`...WithNet40Options` variant). One test method therefore becomes 4-26 test cases.
12 
13`CompilerOptions` flags (see `Helpers/Tester.cs`) select:
14- the compiler: legacy csc (`None`), `UseRoslyn1_3_2`, `UseRoslyn2_10_0`, `UseRoslyn3_11_0`,
15 `UseRoslyn4_14_0`, `UseRoslynLatest` (version comes from `RoslynVersion` in
16 `Directory.Packages.props`), or `UseMcs2_6_4`/`UseMcs5_23`
17- the target: `TargetNet40` (compiles against .NET Framework reference assemblies from the
18 `ILSpy-tests` submodule) vs. .NET Core reference packs (net5.0 for Roslyn 3, current preview
19 for Roslyn 4/latest)
20- codegen options: `Optimize` (`-o+`, defines `OPT`), `UseDebug`, `Force32Bit`, `Library`,
21 `GeneratePdb`, `NullableEnable`, `CheckForOverflowUnderflow`, ...
22 
23Compiled artifacts are named `<TestName><suffix>.exe|dll` where the suffix encodes the config
24(`Tester.GetSuffix`, e.g. `.opt.roslyn3.net40`). By default they land next to the test case
25sources; set `TestsAssemblyTempPath` in `DecompilerTests.config.json` to redirect them
26(`Helpers/TestsAssemblyOutput.cs`).
 
 
 
 
 
27 
28First test run on a machine: the `[SetUpFixture]` in `TestTraceListener.cs` calls
29`Tester.Initialize()`, which downloads the Roslyn toolsets, vswhere, and the reference-assembly
30packs from NuGet (network required; cached under the test output directory afterwards) and
31builds the self-contained `ICSharpCode.Decompiler.TestRunner`. Package downloads check the
32`ILSpy-tests/nuget` folder first, so the `ILSpy-tests` submodule must be initialized (see the
33root `CLAUDE.md` section on the submodule).
34 
35## Test kinds
 
 
 
 
36 
37| Kind | Runner / fixture dir (`TestCases/...`) | Pipeline | Compared against |
38|---|---|---|---|
39| Pretty | `PrettyTestRunner` / `Pretty/*.cs` | compile -> decompile | the test source itself |
40| Correctness | `CorrectnessTestRunner` / `Correctness/*.{cs,vb,il}` | compile -> decompile -> recompile -> execute both | runtime output (stdout/stderr/exit code) of original vs. re-compiled |
41| ILPretty | `ILPrettyTestRunner` / `ILPretty/*.il` | ilasm -> decompile | sibling `.cs` file |
42| Ugly | `UglyTestRunner` / `Ugly/*.cs` | compile -> decompile with sugar settings disabled | sibling `.Expected.cs` file |
43| Disassembler | `DisassemblerPrettyTestRunner` / `Disassembler/Pretty/*.il` | ilasm -> disassemble with our `ReflectionDisassembler` | the `.il` source (or `.expected.il`, e.g. `SortedOutput`) |
44| VBPretty | `VBPrettyTestRunner` / `VBPretty/*.vb` | vbc -> decompile to C# | sibling `.cs` file |
45| PdbGen | `PdbGenerationTestRunner` / `PdbGen/*.cs` | in-proc Roslyn compile (real PDB = oracle), decompile, generate portable PDB with `PortablePdbWriter`, parse both PDBs' sequence-point blobs | the compiler's PDB, projected to the visible breakpoint map (see below) |
46| Roundtrip | `RoundtripAssembly` (inputs from `ILSpy-tests/`) | whole-project decompile -> MSBuild rebuild -> run original NUnit tests against the rebuilt assembly | test-run success |
47| Unit tests | `TypeSystem/`, `Semantics/`, `Output/`, `Util/`, `DataFlowTest`, `Metadata/`, `ProjectDecompiler/` | plain in-process NUnit | assertions |
48 
49## How to add a test
 
 
50 
51Common to the file-based kinds: every file in the fixture directory must have a matching test
52method - each runner has an `AllFilesHaveTests` test that fails otherwise. The method name must
53equal the file name (minus extension); it usually just calls the runner's `Run`/`RunForLibrary`
54helper, which picks the file via `[CallerMemberName]`.
55 
56- **Pretty** (decompiler produces nice code): add `TestCases/Pretty/MyTest.cs` plus a test
57 method choosing the narrowest sensible config group (e.g. C# 8 features need
58 `roslyn3OrNewerOptions`). The file is simultaneously input and expected output, so write it
59 exactly as ILSpy pretty-prints (tabs, `switch {` on the same line, trailing commas, ...).
60 Iterate by running the fixture and adjusting the file to the diff.
61- **Correctness** (decompiled code behaves identically): add
62 `TestCases/Correctness/MyTest.cs` with a `Main` that prints observable state; the harness
63 compiles it, decompiles, re-compiles the decompiled output, executes both, and diffs the
64 output streams. Roslyn-non-net40 configs execute through
65 `ICSharpCode.Decompiler.TestRunner` (an `AssemblyLoadContext` host); other configs run the
66 exe directly.
67- **Ugly** (output with decompiler features switched off still compiles/behaves): add
68 `MyTest.cs` plus the expected decompilation as `MyTest.Expected.cs`.
69- **ILPretty / Disassembler**: add a `.il` file (assembled with the NuGet ilasm) and the
70 expected `.cs` (`ILPretty`) or rely on round-tripping the `.il` itself (`Disassembler`).
71- **VBPretty**: add `MyTest.vb` and the expected C# decompilation `MyTest.cs`.
72- **PdbGen** (the reconstructed PDB's breakpoints match the C# compiler's): add `MyTest.cs`,
73 written exactly as ILSpy pretty-prints a *single type* (no assembly-attribute header - the
74 same discipline as Pretty). The runner compiles it with Roslyn (whose PDB is the oracle),
75 decompiles, reconstructs a PDB, and compares the **breakpoint map**: per method, the
76 ordered source locations of visible sequence points and the placement of hidden sequence
77 points anchored to neighboring source locations. IL offsets, local scopes and the embedded
78 source are all dropped, because the decompiler reconstructs them differently and they never
79 match byte-for-byte. The comparer also runs an oracle-free
80 well-formedness check (strictly increasing IL offsets = no duplicate/overlapping points).
81 `TestSequencePoints()` asserts the map matches the compiler exactly; for the handful of methods
82 where the decompiler legitimately diverges (e.g. it breakpoints a method's opening brace where
83 the compiler keeps it hidden), call `TestSequencePoints(knownResidual: true)`; the residual is
84 auto-derived and committed as `MyTest.residual.txt`, so improvements and regressions both flip
85 the test like a pretty diff. On a mismatch the test writes `MyTest.residual.txt.generated` and
86 fails; accept a deliberate change by re-running with `ILSPY_ACCEPT_PDB_RESIDUAL=1` set (which
87 overwrites the snapshot in place) or by copying the `.generated` file over it. `Tolerance.Lines` drops column comparison
88 for statements whose column placement differs. Nothing is hand-maintained except the `.cs`
89 source and, rarely, the residual snapshot. No `.expected.*` is committed (all regenerated).
90 
91## Conditional expectations (#if) and comparison rules
92 
93Different configs legitimately produce different decompilations. Pretty-style comparisons parse
94both sides with Roslyn using the config's preprocessor symbols and delete inactive `#if`
95regions (`Helpers/CodeAssert.cs`), so test sources can branch on:
96- `OPT` (optimized build), `EXPECTED_OUTPUT` (defined only while comparing, never while
97 compiling - use it for "what ILSpy prints" vs. "equivalent compilable input" differences)
98- compiler family/version: `LEGACY_CSC`, `LEGACY_VBC`, `MCS`, `MCS2`, `MCS5`, `ROSLYN`,
99 `ROSLYN2`, `ROSLYN3`, `ROSLYN4`
100- language version: `CS60` ... `CS130`, `VB11` ... `VB16`
101- target framework: `NET40`, `NETCORE`, `NET50` ... `NET100`
102 
103Normalization before diffing: lines are trimmed, `//` comments stripped, lines starting with
104`#` ignored (so `#pragma`/`#region` in test sources are harmless), blank lines ignored.
105Everything else must match exactly.
106 
107## Probing compiler codegen
 
 
 
 
 
 
 
108 
109To learn how every supported compiler/option combination lowers a construct, add a test case
110exercising it and run the fixture: the harness automatically compiles it with all configured
111compiler versions and settings, and each failing config's diff shows you what the decompiler
112produced for that compiler's IL. This beats hand-running csc versions.
113 
114## Running
 
 
 
 
 
 
 
 
 
 
115 
116```
117dotnet test --solution ILSpy.sln --report-trx --filter FullyQualifiedName~PrettyTestRunner.SwitchExpressions
118```
119 
120(Microsoft.Testing.Platform syntax; see root `CLAUDE.md` "Test discipline".) A failing
121comparison prints an aligned diff with ` + `/` - ` markers. On failure the decompiled output
122file is left on disk for inspection (Correctness failures print its path; output diffs are
123also written to `%TEMP%/<test>.original.out` / `.decompiled.out`).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124 
icsharpcode/ILSpy · CLAUDE.md
@@ +1 @@
1# CLAUDE.md
2 
3Guidance for Claude Code (and future Claude sessions) when working on ILSpy.
4 
5## What this codebase is
6 
7ILSpy 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.
 
 
 
 
8 
9## Tech stack
 
 
 
 
 
 
 
 
10 
11- **Avalonia 12** (not 11.x).
12- **AvaloniaEdit** for the decompiled-code text view.
13- **Dock** (wieslawsoltes/Dock) for the panel layout. NuGet id ≠ CLR namespace — `Dock.Controls.Recycling` lives in `Avalonia.Controls.Recycling`; decompile before guessing xmlns.
14- **Avalonia.Xaml.Behaviors** for attached-behaviour glue.
15- **Avalonia.ExtendedToolkit** (mameolan) for controls not in Avalonia core.
16- **Simple** theme (not Fluent). Check `App.axaml` / csproj before assuming Fluent.
17- **Microsoft.Extensions.DependencyInjection** + **System.Composition** MEF directly, with a small bridge.
18- Central package management is enabled — every `PackageReference` needs a matching `PackageVersion` in `Directory.Packages.props`.
19- Target framework: `net10.0` (cross-platform) for the main app. The test projects target `net11.0` (and `net11.0-windows` for tests that intentionally exercise Windows-only behaviour) so they run on the runtime the `net11.0` preview build SDK ships, without installing a separate `net10.0` runtime in CI. `TestPlugin` stays `net10.0` (it is loaded as a library by the net11 test host, so its TFM need not track the test projects').
20 
21## Project structure
 
 
 
 
 
22 
23Avalonia UI:
24- `ILSpy/` — the Avalonia UI app
25- `ILSpy.Tests/` — headless Avalonia UI tests (Avalonia.Headless.NUnit)
26- `ILSpy.Tests.Windows/` — Windows-only UI tests (OS-gated; `net11.0-windows`)
27- `ILSpy.ReadyToRun/` — ReadyToRun-viewer plugin.
28 
29Cross-platform core (decompiler engine + shared support):
30- `ICSharpCode.Decompiler/` — core decompiler library (multi-targeted, cross-platform)
31- `ICSharpCode.ILSpyX/` — shared UI-host-agnostic support library
32- `ICSharpCode.BamlDecompiler/` — BAML parsing library
33- `ICSharpCode.ILSpyCmd/` — CLI front-end (`ilspycmd`)
34- `ICSharpCode.Decompiler.Tests/` + `ICSharpCode.Decompiler.TestRunner/` — decompiler test suite and its out-of-process runner
35- `ICSharpCode.Decompiler.PowerShell/` — PowerShell cmdlets (`netstandard2.0`)
 
 
 
 
36 
37Test support:
38- `TestPlugin/` — sample plugin exercising the plugin-loading system (`net10.0`)
39- `TestFixtures.Resources/` — generates resource fixtures consumed by the decompiler tests
40 
41Windows-only frontends, packaging, and tests:
42- `ILSpy.AddIn/`, `ILSpy.AddIn.VS2022/` — Visual Studio add-ins (`net472`)
43- `ILSpy.Installer/` — WiX installer (`net472`)
44- `ILSpy.BamlDecompiler.Tests/` — BAML-decompiler tests, still WPF/Windows-bound (`net11.0-windows`)
45 
46Solutions & 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).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47 
48## ILSpy-tests submodule
49 
50- `ILSpy-tests/` is a **git submodule** (`https://github.com/icsharpcode/ILSpy-tests`, branch `master`) 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`).
51- **It is not checked out by default**, because it is large. Tests that need it call `Assert.Ignore` when the directory is absent (see `RoundtripAssembly`/`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).
52- **It opts out of the host build settings on purpose.** The submodule ships its own `ILSpy-tests/Directory.Build.props` that sets `TreatWarningsAsErrors=false`; its mere presence also stops MSBuild's upward `Directory.Build.props` search 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.
53- **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: ...").
 
 
 
 
 
54 
55## Code conventions
 
 
56 
57- **File headers: every new `.cs` file starts with the standard MIT X11 license header.** Line 1 is `// Copyright (c) <current year> <contributor's name>` — the name of the human contributing the change (e.g. `// Copyright (c) 2026 Siegfried Pammer`), never AlphaSierraPapa and never an AI agent. It is followed by the repo's verbatim permission/warranty text in `//` comments — copy it exactly from an existing file (e.g. `ILSpy/NavigationEntry.cs`), then a blank line, then the usings. Never rewrite the header of an existing file: they keep their original copyright holder (AlphaSierraPapa, another contributor, `ICSharpCode.BamlDecompiler`'s block-comment variant) or belong to vendored third-party code with its own license (`Humanizer`, Tunnel Vision Laboratories), and older header-less files stay header-less — don't add headers retroactively as churn.
58- **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.
59- **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.
60- **TDD for new features.** Write the failing test, show it red, implement, show it green. Never skip the red step.
61- **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).
62- **No silent returns in tests.** When an expected component is missing, assert and fail. Never `return` early to bypass the assertion.
63- **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.
64- **en-US English** in all source code, comments, identifiers, and log/error strings.
65 
66## Build / restore
 
 
 
67 
68- **Use the repo-root pwsh scripts for these tasks, not raw `dotnet` commands** -- they carry the flags that keep the repo consistent and all target `ILSpy.sln` (extra args are forwarded):
69 - **restore** -> `restore.ps1`
70 - **build** -> `build.ps1` (`-Configuration Debug|Release`)
71 - **update deps / regenerate lock files** -> `updatedeps.ps1`
72 - **format** -> `BuildTools/format.ps1`
73 - **clean** -> `clean.ps1`; **publish** -> `publish.ps1`
74- **Why this matters:** `restore.ps1` and `updatedeps.ps1` pass `-p:RestoreEnablePackagePruning=false`, so they keep every `packages.lock.json` whole. A bare `dotnet 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 spurious `packages.lock.json` diff. Restore with `restore.ps1`, then build with `build.ps1 --no-restore`; if a bare build ever leaves a pruned `packages.lock.json` modified, discard it (`git checkout -- <path>`).
75- **Every project generates a `packages.lock.json`** (`RestorePackagesWithLockFile` is set in the root `Directory.Build.props`); CI NuGet caching keys off these. After adding or bumping a `PackageReference`/`PackageVersion`, regenerate the lock files with `updatedeps.ps1` (i.e. `dotnet restore ILSpy.sln --force-evaluate -p:RestoreEnablePackagePruning=false`) and commit them. The core libraries additionally set `RestoreLockedMode`, so a plain restore there fails until the lock file is refreshed.
76- The pre-commit hook runs `dotnet format` on the **whole solution** -- it IS the formatter. **Always let the hook run; never commit `.cs` with `--no-verify`.** Bypassing it lands unformatted code and forces history-wide reformat rebases later. `--no-verify` is acceptable only for commits that touch no `.cs` (e.g. `.yml`/`.md`-only).
77- **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's `git add -u` doesn't churn EOLs. On **Linux/macOS**, leave new files LF and do **not** `unix2dos` -- forcing CRLF there makes the whole working tree show as phantom-modified in `git status` (and is normalized back to LF on commit anyway).
78- **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.
79 
80## Commit workflow
 
 
81 
82- **Subject** is a succinct phrase describing the change. Target <= 72 chars. No area prefix; the subject itself should make the change clear.
83- **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.
84- `Fix #NNNN: ...` closes an issue. `#NNNN` references one without closing.
85- **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.
86- **en-US English** in subject and body. ASCII-only unless a non-ASCII character is genuinely required for what the message describes.
87- **AI attribution: use `Assisted-by:`, not `Co-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 form `Assisted-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 a `Co-Authored-By:` line for the AI, and an AI agent **must not** add a `Signed-off-by:` (only a human can certify the DCO).
88 
89## Test discipline
90 
91- Always run the test suite with `--report-trx` so failures survive: `dotnet test --solution ILSpy.sln --report-trx` (the repo pins Microsoft.Testing.Platform in `global.json`; the bare `dotnet test <sln>` form is the old VSTest syntax). Don't dismiss failures as flaky without first reproducing in isolation, then running repeatedly.
92- The decompiler test suite (test kinds, fixture structure, how to write tests, the compiler-matrix model) is documented in [ICSharpCode.Decompiler.Tests/CLAUDE.md](ICSharpCode.Decompiler.Tests/CLAUDE.md).
93- After matcher / rewriter edits, **run the relevant tests, not just the build.** `dotnet build` green ≠ behaviour correct.
94 
95## Investigating dependencies
96 
97- **Decompile NuGet packages with this repo's `ilspycmd`** to inspect dependency internals — don't grep binaries.
98 
99## Onboarding new contributors
100 
101If 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.
102 
@@ −1 +1 @@
1−# ICSharpCode.Decompiler.Tests guide
1+# CLAUDE.md
22  
3−How the decompiler test suite is structured, what each test kind does, and how to add tests.
3+Guidance for Claude Code (and future Claude sessions) when working on ILSpy.
44  
5−## The matrix-testing model
5+## What this codebase is
66  
7−Most fixtures run one logical test against a whole matrix of compilers and options: an NUnit
8−`[Test]` method takes a `CompilerOptions` parameter fed by `[ValueSource]` from static config
9−arrays declared per runner (`defaultOptions` including mcs, `roslynOnlyOptions`,
10−`roslyn2OrNewerOptions`, `roslyn3OrNewerOptions`, `roslyn4OrNewerOptions`, each also in a
11−`...WithNet40Options` variant). One test method therefore becomes 4-26 test cases.
7+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.
128  
13−`CompilerOptions` flags (see `Helpers/Tester.cs`) select:
14−- the compiler: legacy csc (`None`), `UseRoslyn1_3_2`, `UseRoslyn2_10_0`, `UseRoslyn3_11_0`,
15− `UseRoslyn4_14_0`, `UseRoslynLatest` (version comes from `RoslynVersion` in
16− `Directory.Packages.props`), or `UseMcs2_6_4`/`UseMcs5_23`
17−- the target: `TargetNet40` (compiles against .NET Framework reference assemblies from the
18− `ILSpy-tests` submodule) vs. .NET Core reference packs (net5.0 for Roslyn 3, current preview
19− for Roslyn 4/latest)
20−- codegen options: `Optimize` (`-o+`, defines `OPT`), `UseDebug`, `Force32Bit`, `Library`,
21− `GeneratePdb`, `NullableEnable`, `CheckForOverflowUnderflow`, ...
9+## Tech stack
2210  
23−Compiled artifacts are named `<TestName><suffix>.exe|dll` where the suffix encodes the config
24−(`Tester.GetSuffix`, e.g. `.opt.roslyn3.net40`). By default they land next to the test case
25−sources; set `TestsAssemblyTempPath` in `DecompilerTests.config.json` to redirect them
26−(`Helpers/TestsAssemblyOutput.cs`).
11+- **Avalonia 12** (not 11.x).
12+- **AvaloniaEdit** for the decompiled-code text view.
13+- **Dock** (wieslawsoltes/Dock) for the panel layout. NuGet id ≠ CLR namespace — `Dock.Controls.Recycling` lives in `Avalonia.Controls.Recycling`; decompile before guessing xmlns.
14+- **Avalonia.Xaml.Behaviors** for attached-behaviour glue.
15+- **Avalonia.ExtendedToolkit** (mameolan) for controls not in Avalonia core.
16+- **Simple** theme (not Fluent). Check `App.axaml` / csproj before assuming Fluent.
17+- **Microsoft.Extensions.DependencyInjection** + **System.Composition** MEF directly, with a small bridge.
18+- Central package management is enabled — every `PackageReference` needs a matching `PackageVersion` in `Directory.Packages.props`.
19+- Target framework: `net10.0` (cross-platform) for the main app. The test projects target `net11.0` (and `net11.0-windows` for tests that intentionally exercise Windows-only behaviour) so they run on the runtime the `net11.0` preview build SDK ships, without installing a separate `net10.0` runtime in CI. `TestPlugin` stays `net10.0` (it is loaded as a library by the net11 test host, so its TFM need not track the test projects').
2720  
28−First test run on a machine: the `[SetUpFixture]` in `TestTraceListener.cs` calls
29−`Tester.Initialize()`, which downloads the Roslyn toolsets, vswhere, and the reference-assembly
30−packs from NuGet (network required; cached under the test output directory afterwards) and
31−builds the self-contained `ICSharpCode.Decompiler.TestRunner`. Package downloads check the
32−`ILSpy-tests/nuget` folder first, so the `ILSpy-tests` submodule must be initialized (see the
33−root `CLAUDE.md` section on the submodule).
21+## Project structure
3422  
35−## Test kinds
23+Avalonia UI:
24+- `ILSpy/` — the Avalonia UI app
25+- `ILSpy.Tests/` — headless Avalonia UI tests (Avalonia.Headless.NUnit)
26+- `ILSpy.Tests.Windows/` — Windows-only UI tests (OS-gated; `net11.0-windows`)
27+- `ILSpy.ReadyToRun/` — ReadyToRun-viewer plugin.
3628  
37−| Kind | Runner / fixture dir (`TestCases/...`) | Pipeline | Compared against |
38−|---|---|---|---|
39−| Pretty | `PrettyTestRunner` / `Pretty/*.cs` | compile -> decompile | the test source itself |
40−| Correctness | `CorrectnessTestRunner` / `Correctness/*.{cs,vb,il}` | compile -> decompile -> recompile -> execute both | runtime output (stdout/stderr/exit code) of original vs. re-compiled |
41−| ILPretty | `ILPrettyTestRunner` / `ILPretty/*.il` | ilasm -> decompile | sibling `.cs` file |
42−| Ugly | `UglyTestRunner` / `Ugly/*.cs` | compile -> decompile with sugar settings disabled | sibling `.Expected.cs` file |
43−| Disassembler | `DisassemblerPrettyTestRunner` / `Disassembler/Pretty/*.il` | ilasm -> disassemble with our `ReflectionDisassembler` | the `.il` source (or `.expected.il`, e.g. `SortedOutput`) |
44−| VBPretty | `VBPrettyTestRunner` / `VBPretty/*.vb` | vbc -> decompile to C# | sibling `.cs` file |
45−| PdbGen | `PdbGenerationTestRunner` / `PdbGen/*.cs` | in-proc Roslyn compile (real PDB = oracle), decompile, generate portable PDB with `PortablePdbWriter`, parse both PDBs' sequence-point blobs | the compiler's PDB, projected to the visible breakpoint map (see below) |
46−| Roundtrip | `RoundtripAssembly` (inputs from `ILSpy-tests/`) | whole-project decompile -> MSBuild rebuild -> run original NUnit tests against the rebuilt assembly | test-run success |
47−| Unit tests | `TypeSystem/`, `Semantics/`, `Output/`, `Util/`, `DataFlowTest`, `Metadata/`, `ProjectDecompiler/` | plain in-process NUnit | assertions |
29+Cross-platform core (decompiler engine + shared support):
30+- `ICSharpCode.Decompiler/` — core decompiler library (multi-targeted, cross-platform)
31+- `ICSharpCode.ILSpyX/` — shared UI-host-agnostic support library
32+- `ICSharpCode.BamlDecompiler/` — BAML parsing library
33+- `ICSharpCode.ILSpyCmd/` — CLI front-end (`ilspycmd`)
34+- `ICSharpCode.Decompiler.Tests/` + `ICSharpCode.Decompiler.TestRunner/` — decompiler test suite and its out-of-process runner
35+- `ICSharpCode.Decompiler.PowerShell/` — PowerShell cmdlets (`netstandard2.0`)
4836  
49−## How to add a test
37+Test support:
38+- `TestPlugin/` — sample plugin exercising the plugin-loading system (`net10.0`)
39+- `TestFixtures.Resources/` — generates resource fixtures consumed by the decompiler tests
5040  
51−Common to the file-based kinds: every file in the fixture directory must have a matching test
52−method - each runner has an `AllFilesHaveTests` test that fails otherwise. The method name must
53−equal the file name (minus extension); it usually just calls the runner's `Run`/`RunForLibrary`
54−helper, which picks the file via `[CallerMemberName]`.
41+Windows-only frontends, packaging, and tests:
42+- `ILSpy.AddIn/`, `ILSpy.AddIn.VS2022/` — Visual Studio add-ins (`net472`)
43+- `ILSpy.Installer/` — WiX installer (`net472`)
44+- `ILSpy.BamlDecompiler.Tests/` — BAML-decompiler tests, still WPF/Windows-bound (`net11.0-windows`)
5545  
56−- **Pretty** (decompiler produces nice code): add `TestCases/Pretty/MyTest.cs` plus a test
57− method choosing the narrowest sensible config group (e.g. C# 8 features need
58− `roslyn3OrNewerOptions`). The file is simultaneously input and expected output, so write it
59− exactly as ILSpy pretty-prints (tabs, `switch {` on the same line, trailing commas, ...).
60− Iterate by running the fixture and adjusting the file to the diff.
61−- **Correctness** (decompiled code behaves identically): add
62− `TestCases/Correctness/MyTest.cs` with a `Main` that prints observable state; the harness
63− compiles it, decompiles, re-compiles the decompiled output, executes both, and diffs the
64− output streams. Roslyn-non-net40 configs execute through
65− `ICSharpCode.Decompiler.TestRunner` (an `AssemblyLoadContext` host); other configs run the
66− exe directly.
67−- **Ugly** (output with decompiler features switched off still compiles/behaves): add
68− `MyTest.cs` plus the expected decompilation as `MyTest.Expected.cs`.
69−- **ILPretty / Disassembler**: add a `.il` file (assembled with the NuGet ilasm) and the
70− expected `.cs` (`ILPretty`) or rely on round-tripping the `.il` itself (`Disassembler`).
71−- **VBPretty**: add `MyTest.vb` and the expected C# decompilation `MyTest.cs`.
72−- **PdbGen** (the reconstructed PDB's breakpoints match the C# compiler's): add `MyTest.cs`,
73− written exactly as ILSpy pretty-prints a *single type* (no assembly-attribute header - the
74− same discipline as Pretty). The runner compiles it with Roslyn (whose PDB is the oracle),
75− decompiles, reconstructs a PDB, and compares the **breakpoint map**: per method, the
76− ordered source locations of visible sequence points and the placement of hidden sequence
77− points anchored to neighboring source locations. IL offsets, local scopes and the embedded
78− source are all dropped, because the decompiler reconstructs them differently and they never
79− match byte-for-byte. The comparer also runs an oracle-free
80− well-formedness check (strictly increasing IL offsets = no duplicate/overlapping points).
81− `TestSequencePoints()` asserts the map matches the compiler exactly; for the handful of methods
82− where the decompiler legitimately diverges (e.g. it breakpoints a method's opening brace where
83− the compiler keeps it hidden), call `TestSequencePoints(knownResidual: true)`; the residual is
84− auto-derived and committed as `MyTest.residual.txt`, so improvements and regressions both flip
85− the test like a pretty diff. On a mismatch the test writes `MyTest.residual.txt.generated` and
86− fails; accept a deliberate change by re-running with `ILSPY_ACCEPT_PDB_RESIDUAL=1` set (which
87− overwrites the snapshot in place) or by copying the `.generated` file over it. `Tolerance.Lines` drops column comparison
88− for statements whose column placement differs. Nothing is hand-maintained except the `.cs`
89− source and, rarely, the residual snapshot. No `.expected.*` is committed (all regenerated).
46+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).
9047  
91−## Conditional expectations (#if) and comparison rules
48+## ILSpy-tests submodule
9249  
93−Different configs legitimately produce different decompilations. Pretty-style comparisons parse
94−both sides with Roslyn using the config's preprocessor symbols and delete inactive `#if`
95−regions (`Helpers/CodeAssert.cs`), so test sources can branch on:
96−- `OPT` (optimized build), `EXPECTED_OUTPUT` (defined only while comparing, never while
97− compiling - use it for "what ILSpy prints" vs. "equivalent compilable input" differences)
98−- compiler family/version: `LEGACY_CSC`, `LEGACY_VBC`, `MCS`, `MCS2`, `MCS5`, `ROSLYN`,
99− `ROSLYN2`, `ROSLYN3`, `ROSLYN4`
100−- language version: `CS60` ... `CS130`, `VB11` ... `VB16`
101−- target framework: `NET40`, `NETCORE`, `NET50` ... `NET100`
50+- `ILSpy-tests/` is a **git submodule** (`https://github.com/icsharpcode/ILSpy-tests`, branch `master`) 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`).
51+- **It is not checked out by default**, because it is large. Tests that need it call `Assert.Ignore` when the directory is absent (see `RoundtripAssembly`/`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).
52+- **It opts out of the host build settings on purpose.** The submodule ships its own `ILSpy-tests/Directory.Build.props` that sets `TreatWarningsAsErrors=false`; its mere presence also stops MSBuild's upward `Directory.Build.props` search 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.
53+- **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: ...").
10254  
103−Normalization before diffing: lines are trimmed, `//` comments stripped, lines starting with
104−`#` ignored (so `#pragma`/`#region` in test sources are harmless), blank lines ignored.
105−Everything else must match exactly.
55+## Code conventions
10656  
107−## Probing compiler codegen
57+- **File headers: every new `.cs` file starts with the standard MIT X11 license header.** Line 1 is `// Copyright (c) <current year> <contributor's name>` — the name of the human contributing the change (e.g. `// Copyright (c) 2026 Siegfried Pammer`), never AlphaSierraPapa and never an AI agent. It is followed by the repo's verbatim permission/warranty text in `//` comments — copy it exactly from an existing file (e.g. `ILSpy/NavigationEntry.cs`), then a blank line, then the usings. Never rewrite the header of an existing file: they keep their original copyright holder (AlphaSierraPapa, another contributor, `ICSharpCode.BamlDecompiler`'s block-comment variant) or belong to vendored third-party code with its own license (`Humanizer`, Tunnel Vision Laboratories), and older header-less files stay header-less — don't add headers retroactively as churn.
58+- **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.
59+- **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.
60+- **TDD for new features.** Write the failing test, show it red, implement, show it green. Never skip the red step.
61+- **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).
62+- **No silent returns in tests.** When an expected component is missing, assert and fail. Never `return` early to bypass the assertion.
63+- **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.
64+- **en-US English** in all source code, comments, identifiers, and log/error strings.
10865  
109−To learn how every supported compiler/option combination lowers a construct, add a test case
110−exercising it and run the fixture: the harness automatically compiles it with all configured
111−compiler versions and settings, and each failing config's diff shows you what the decompiler
112−produced for that compiler's IL. This beats hand-running csc versions.
66+## Build / restore
11367  
114−## Running
68+- **Use the repo-root pwsh scripts for these tasks, not raw `dotnet` commands** -- they carry the flags that keep the repo consistent and all target `ILSpy.sln` (extra args are forwarded):
69+ - **restore** -> `restore.ps1`
70+ - **build** -> `build.ps1` (`-Configuration Debug|Release`)
71+ - **update deps / regenerate lock files** -> `updatedeps.ps1`
72+ - **format** -> `BuildTools/format.ps1`
73+ - **clean** -> `clean.ps1`; **publish** -> `publish.ps1`
74+- **Why this matters:** `restore.ps1` and `updatedeps.ps1` pass `-p:RestoreEnablePackagePruning=false`, so they keep every `packages.lock.json` whole. A bare `dotnet 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 spurious `packages.lock.json` diff. Restore with `restore.ps1`, then build with `build.ps1 --no-restore`; if a bare build ever leaves a pruned `packages.lock.json` modified, discard it (`git checkout -- <path>`).
75+- **Every project generates a `packages.lock.json`** (`RestorePackagesWithLockFile` is set in the root `Directory.Build.props`); CI NuGet caching keys off these. After adding or bumping a `PackageReference`/`PackageVersion`, regenerate the lock files with `updatedeps.ps1` (i.e. `dotnet restore ILSpy.sln --force-evaluate -p:RestoreEnablePackagePruning=false`) and commit them. The core libraries additionally set `RestoreLockedMode`, so a plain restore there fails until the lock file is refreshed.
76+- The pre-commit hook runs `dotnet format` on the **whole solution** -- it IS the formatter. **Always let the hook run; never commit `.cs` with `--no-verify`.** Bypassing it lands unformatted code and forces history-wide reformat rebases later. `--no-verify` is acceptable only for commits that touch no `.cs` (e.g. `.yml`/`.md`-only).
77+- **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's `git add -u` doesn't churn EOLs. On **Linux/macOS**, leave new files LF and do **not** `unix2dos` -- forcing CRLF there makes the whole working tree show as phantom-modified in `git status` (and is normalized back to LF on commit anyway).
78+- **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.
11579  
116−```
117−dotnet test --solution ILSpy.sln --report-trx --filter FullyQualifiedName~PrettyTestRunner.SwitchExpressions
118−```
80+## Commit workflow
11981  
120−(Microsoft.Testing.Platform syntax; see root `CLAUDE.md` "Test discipline".) A failing
121−comparison prints an aligned diff with ` + `/` - ` markers. On failure the decompiled output
122−file is left on disk for inspection (Correctness failures print its path; output diffs are
123−also written to `%TEMP%/<test>.original.out` / `.decompiled.out`).
82+- **Subject** is a succinct phrase describing the change. Target <= 72 chars. No area prefix; the subject itself should make the change clear.
83+- **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.
84+- `Fix #NNNN: ...` closes an issue. `#NNNN` references one without closing.
85+- **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.
86+- **en-US English** in subject and body. ASCII-only unless a non-ASCII character is genuinely required for what the message describes.
87+- **AI attribution: use `Assisted-by:`, not `Co-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 form `Assisted-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 a `Co-Authored-By:` line for the AI, and an AI agent **must not** add a `Signed-off-by:` (only a human can certify the DCO).
88+ 
89+## Test discipline
90+ 
91+- Always run the test suite with `--report-trx` so failures survive: `dotnet test --solution ILSpy.sln --report-trx` (the repo pins Microsoft.Testing.Platform in `global.json`; the bare `dotnet test <sln>` form is the old VSTest syntax). Don't dismiss failures as flaky without first reproducing in isolation, then running repeatedly.
92+- The decompiler test suite (test kinds, fixture structure, how to write tests, the compiler-matrix model) is documented in [ICSharpCode.Decompiler.Tests/CLAUDE.md](ICSharpCode.Decompiler.Tests/CLAUDE.md).
93+- After matcher / rewriter edits, **run the relevant tests, not just the build.** `dotnet build` green ≠ behaviour correct.
94+ 
95+## Investigating dependencies
96+ 
97+- **Decompile NuGet packages with this repo's `ilspycmd`** to inspect dependency internals — don't grep binaries.
98+ 
99+## Onboarding new contributors
100+ 
101+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.
124102  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack