Commit Graph
83 Commits
Author SHA1 Message Date
Siegfried Pammer d22d4301e4 Expand nested conditional operators back into if-else
HandleConditionalOperator collapses `if (c) a = x; else a = y;` into a
conditional operator, innermost first, and keeps going for as long as the
chain does. A source else-if ladder therefore comes back as one expression,
however long it was: the sample in #2027 decompiles to a single
2095-character statement, and one NLog method to 2230 characters nested 29
brackets deep.

ExpandNestedConditionals undoes that past one level, so a statement keeps at
most a single conditional operator.

It runs at the end of the pipeline rather than inside ExpressionTransforms,
because every transform that needs its input to be a single expression has
to see the collapsed form first: object and collection initializers, `with`,
switch expressions, interpolated string handlers, and the query lambdas the
C# stage later rewrites into clauses. Cutting the chain earlier leaves an
if-else between the statements they pattern-match on and they silently stop
matching - an object initializer assigning an init-only member then does not
even compile. The same reasoning ReduceNestingTransform gives for walking
back ConditionDetection's aggressive else-inlining once the structure is
settled.

A chain already stored to a variable is expanded into that variable, so
nothing has to be decided: the variable carries its own type. A chain in any
other position - an argument, a return value, a field store - has nothing to
expand into, and ILExtraction can give it one. The temporary ILExtraction
creates is typed from the stack type though, where `I4` is `int`, `bool`,
`char` and every enum at once, so extracting on that basis turned a bool
into `int num` with `if (num == 0)` and an enum into
`dbType = (IsFixedLength ? 22 : 0)`.

InferExpectedType is the counterpart to InferType that answers this: where
InferType asks what a value is, it asks what the position the value flows
into says it should be - a parameter, a return type, a field, all of which
carry their type in metadata. Extraction is done only where that question
has an answer, and the temporary is typed from it. The receiver of a call
then reads `XPathNavigator xPathNavigator`, not `object obj` with a cast
back, and a field store keeps its enum's member names.

The Pretty fixture covers what must NOT change: an array initializer, a
query lambda, a ref local, a switch expression, an object initializer with
an init-only member, a `with` expression, a catch-when filter and both
constructor-initializer forms. The positive case is an ILPretty test,
because a Pretty fixture is its own input and expected output, and a chain
that round-trips through collapse and expansion has no fixed point there.
The PdbGen test records the cost in breakpoints: the compiler's single
sequence point for the collapsed statement becomes one per expanded
statement, which is inherent to splitting a statement in two.

#2027

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-30 00:20:51 +02:00
Siegfried Pammer 0447e2b58f Merge pull request #3930 from icsharpcode/tests/829-first-class-span
Decompile C# 14 implicit span conversions (first-class span types)
2026-08-15 20:34:42 +02:00
Siegfried Pammer 63d03f1b49 Fix #1142: recover enum members for out-of-range constant operands
An addition or subtraction on an enum whose constant operand's numeric
value does not fit the underlying type (an int constant standing for a
high uint member, e.g. -501 for 0xfffffe0b) failed to resolve as enum
arithmetic and fell back to integer arithmetic with casts, producing
'(uint)((int)value - -501)' and, for the same source expression in an
argument position, '(uint)value - 4294966795u'. Retry the failed
resolution once with constant operands reinterpreted in the enum type;
the reinterpretation is lossless whenever the constant's stack type
matches the enum's underlying stack type, because the IL constant is
the member's bit pattern. Valid non-enum resolutions like 'data - 1'
(enum minus underlying, yielding the enum) are unaffected because the
retry only runs when the plain resolution fails.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-15 18:57:38 +02:00
Siegfried Pammer cd181b3f08 Fold C# 14 implicit span conversions instead of printing their helpers
The C# 14 compiler lowers implicit span conversions to calls -
MemoryExtensions.AsSpan(string), ReadOnlySpan<T>.CastUp, and the span
op_Implicit operators - so decompiled code showed the lowered form even
though the conversion and betterness layers already implement the C# 14
rules. CallBuilder now folds those helper calls back into conversions,
riding the existing mechanism: the conversion is built as an explicit
cast, consumption sites make it implicit where the context allows, and
the overload-resolution recheck re-adds a cast when the bare argument
would bind to a different overload (which canonicalizes deliberate
AsSpan disambiguations to the equivalent explicit span cast).

Span conversions compose, so CastCanBeMadeImplicit lets a direct
input-to-target span conversion replace a chained pair; and an rvalue
bound to an in parameter gets the same chance to shed the cast as a
by-value argument, since ChangeDirectionExpressionTo bypasses the
by-value strip.

Part of #829.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-15 18:42:53 +02:00
Daniel Grunwald 865cf1e534 Merge pull request #3995 from icsharpcode/fix/instance-operator-call-resolution
Report a call as unresolvable when nothing matched at all
2026-08-15 09:35:11 +02:00
Siegfried Pammer ac8a8ff64d Add an ILPretty test for #3729
Structs whose assembly is missing decompile through the unresolved-type
path, which is not covered anywhere: the fixture pins the constructor
shapes that path has to recognize, and the reference-type cases that must
keep falling through to a plain call.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-14 19:12:02 +02:00
Siegfried Pammer a9c4f6c6bb Report a call as unresolvable when nothing matched at all
Overload resolution reports no error for an empty candidate set - there is
no best candidate to attach one to - so the null result passed for success
and was dereferenced while checking the call target. Decompiling
FSharp.DataFrame from nuget.org crashes that way: F# compiles its comparison
members to instance methods carrying operator metadata names, and the
operator candidate search looks at the operand types rather than at the
receiver type the member belongs to.

The new fixture pins that such an assembly decompiles at all. It still
renders those instance methods as operators and drops the receiver at the
call sites, which is the misclassification behind the empty candidate set
and is handled separately; this is the guard that keeps an empty candidate
set from being read as a resolved call.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-14 10:44:19 +02:00
Siegfried Pammer 60c08fcb74 Keep 'delegate {}' when a parameter type is inaccessible
The parameter-list-less anonymous method form is compatible with any
delegate signature, and C# code must rely on exactly that when a
delegate's parameter types cannot be named at the use site: IL, unlike
C#, permits a delegate signature to reference less accessible types.
Expanding such an anonymous method into a lambda would force the
unnameable type into a parameter list. Keep the delegate form, with its
parameter list dropped, when the parameters are unused and one of their
types is not accessible from the current context.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 10:54:52 +02:00
Siegfried Pammer 3cc49044ec Extend UseLambdaSyntax to emit statement lambdas
Under UseLambdaSyntax, anonymous functions became lambdas only when an
expression body was possible; statement-bodied ones kept C# 2 delegate
syntax. Now every anonymous function whose parameter shape a lambda can
express uses lambda syntax; delegate syntax remains for ref/out/in and
params parameters and for pre-C# 3 language profiles.

Two latent issues surfaced by the wider lambda coverage: DeclareVariables
assumed an insertion point directly under a LambdaExpression is an
expression body it must convert to a block, which block-bodied lambdas
now violate; and anonymous methods declared without a parameter list
carry compiler-generated parameter names like '<p0>' that are not valid
identifiers, so the lambda's mandatory parameter list regenerates such
names from the parameter type: (object obj, EventArgs e) => ...
A side effect visible in fixtures: an explicit parameter list can make
a delegate-creation cast redundant that bare 'delegate' syntax needed
for overload resolution, e.g. new Thread((ThreadStart)delegate { })
becomes new Thread(() => { }).

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 10:54:52 +02:00
Siegfried Pammer e8346dc58c Recognize a protected copy constructor on a sealed record
IsCopyConstructor required the copy constructor to be private on a sealed
record and protected otherwise, but IsGeneratedCopyConstructor in the same
class accepts protected regardless of sealedness. Since the former gates the
latter, a sealed record whose copy constructor stayed protected -- what you get
when a record is sealed after it was compiled -- was not recognized as one at
all: it fell through to the general constructor handling, where its base call
made it count as unchained, and the primary-constructor invariant then failed.

Found by fuzzing nuget.org. Beyond silencing the assertion this improves the
output, as the affected types now decompile to positional records.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-07 07:05:04 +02:00
Siegfried Pammer d9c60f08b0 Fix crash decompiling reference assemblies with stripped method bodies
The accessor forwarding-stub matcher bounded the IL body from above but not
from below. Reference assemblies keep the method RVA while stripping the body
to zero bytes, so an explicit interface accessor there sailed past the size
check and the first opcode read ran off the end of the blob, aborting the
whole type with a BadImageFormatException. The sibling matcher in
TransformDisplayClassUsage already guards its lower bound; this one did not.

Found by fuzzing nuget.org: every package resolving to the
Microsoft.NETFramework.ReferenceAssemblies packs was affected.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-07 06:09:09 +02:00
Siegfried Pammer c0aa2db0f8 Add regression test for property rows without accessors
Corrupt or hand-written metadata can contain a property with no
MethodSemantics rows at all. MetadataTypeDefinition.Properties already
skips properties with neither a visible getter nor setter, so neither
the parameterized-property path nor the ordinary one ever sees them;
the accessor-method emission also tolerates a missing accessor by
construction. Pin that with an ILPretty case containing both a
parameterized and an ordinary accessor-less property.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:28:34 +02:00
Siegfried Pammer cac1209014 Fix #1638: identify renamed yield state machine members via method impls
Obfuscators may rename the members of compiler-generated iterator
classes while keeping the MethodImpl (.override) rows intact. Since
b110d5c2d (first shipped in ILSpy 8.0), Dispose, get_Current and
GetEnumerator are identified by name or method impl; MoveNext was still
looked up by name only, so such a state machine was rejected with
"Method not found". The pre-Roslyn "yield break" handling likewise
matched the Dispose call by name; it now compares against the already
resolved Dispose handle. The new ILPretty fixture models the obfuscated
shape from the issue's assemblies, against which the fix was verified;
the rest of the issue was already resolved by b110d5c2d and 81e702f84.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-22 21:30:53 +02:00
Sebastien Lebreton b0e446618f Fix #3850: recover ReadOnlySpan<T> array literals from the legacy lazy cache
Roslyn caches a ReadOnlySpan<T> created from an array literal in a
<PrivateImplementationDetails> field on target frameworks without
RuntimeHelpers.CreateSpan (e.g. .NET Framework / netstandard2.0 + System.Memory):

    object obj = <PrivateImplementationDetails>.cache;
    if (obj == null) {
        obj = new char[] { '\r', '\n' };
        <PrivateImplementationDetails>.cache = (char[])obj;
    }
    ... new ReadOnlySpan<char>((char[])obj) ...

The decompiled output referenced the compiler-synthesized
<PrivateImplementationDetails> type, whose escaped name is not expressible in C#
and is never declared, so the output failed to recompile (CS0400).

The modern RuntimeHelpers.CreateSpan form was already handled
(TransformRuntimeHelpersCreateSpanInitialization); this adds the analogous
handling for the legacy lazy-cache form, mirroring CachedDelegateInitialization
(which collapses the same lazy-static-field cache for anonymous-method delegates).
Once the cache is collapsed, the existing array-initializer transforms recover
the array literal, so the <PrivateImplementationDetails> reference disappears.

Test: ILPretty/CachedReadOnlySpanInitialization.
2026-07-13 11:12:10 +02:00
Siegfried Pammer 15719eedd6 Reject negative char index in the length-and-char string switch
MatchSwitchOnCharBlock's case 2 and default paths guarded against a
negative character index, but case 1 (a bare switch on get_Chars) did
not. Crafted IL whose get_Chars/get_Item index is negative - a value no
compiler emits, but valid IL - therefore reached the pattern
reconstruction unchecked. For a length-1 group this silently miscompiled
the switch (it rebuilds the string switch from the char labels without
using the index), turning IL that reads s[-1] into `switch (s)`; for
longer strings it threw IndexOutOfRangeException and aborted the method.

Move the check into MatchGetChars so all three call sites reject a
negative index by construction, and drop the two now-redundant guards.

Same class of unvalidated-integer robustness issue as #3878, in a
different switch-on-string pattern.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-13 05:53:16 +02:00
Siegfried Pammer a308b64a7f Add ILPretty regression test for duplicate stackalloc store
A stackalloc initializer whose first element is written twice must not be
folded into a 'stackalloc int[] { ... }': doing so drops the earlier store,
which is observable when its value has side effects. HandleSequentialLocAllocInitializer
already rejects this (the extra store trips the LoadCount and elementCount
guards); the sequential-store offset matcher on its own would misclassify the
second offset-0 write. The IL is derived from a C# 'stackalloc int[] { a, b, c }'
disassembly with an extra offset-0 store injected, a shape no compiler emits.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-07 09:52:17 +02:00
Siegfried Pammer 99888eb78e Surface the IL 'tail.' prefix as an inline comment in C# output
F# emits tail calls pervasively, but the 'tail.' prefix was dropped
entirely at the C# output stage, so the information never reached the
decompiled text. Render it inline as '/*tail.*/' before the call,
mirroring the existing 'constrained.' prefix comment in CallBuilder.

Fix #3817

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-24 21:39:15 +02:00
Siegfried Pammer 679cf0ee6a Use cross-platform separators for the FSharp.Core.dll test path
CopyFSharpCoreDll built the ILSpy-tests path with hardcoded Windows backslashes
("..\\..\\..\\ILSpy-tests\\FSharp\\FSharp.Core.dll"). On non-Windows, '\' is a
normal filename character, so the whole string became a single bogus path,
File.Exists always returned false, and the FSharp ILPretty tests were silently
Assert.Ignore'd even when ILSpy-tests was checked out. Build the path from
Path.Combine segments instead so it resolves on every platform (same target:
three levels up from the ILPretty test-case directory).

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-16 09:49:28 +02:00
Siegfried Pammer 0f9de78f02 Run the decompiler test suite on Linux
Compilation uses the .NET builds of the Roslyn toolsets (tasks/netcore*,
bincore csc.dll/vbc.dll launched through the dotnet host). ilasm/ildasm
options use the '-' prefix, which all platforms accept. The dotnet-hosted
compilers have no implicit references or SDK path: net40 compiles pass
mscorlib explicitly, and vbc gets -sdkpath, _MYTYPE=Empty and
-vbruntime:Microsoft.VisualBasic.Core.dll (the facade in the ref packs is
not followed for runtime helpers). The TestRunner gets a self-contained
build for the host platform.

Configurations depending on Windows-only tools or runtimes (legacy
csc/vbc, Roslyn 1.x/2.x, mcs, Force32Bit, executing net40 binaries) are
filtered from the matrix off-Windows via Tester.SupportedOnCurrentPlatform
or gated with [Platform("Win")]. PdbGen comparisons normalize document
name separators, and Correctness/Async uses Console.IsInputRedirected
instead of the Windows-only Console.CapsLock.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-06-11 23:36:32 +02:00
danbopesandClaude Opus 4.7 01b968fbbd Add opt-in SortSwitchSections setting (#3749). (#3750)
When enabled, switch sections are ordered by their case label value
instead of by the underlying branch's IL offset. Default is false to
keep existing output unchanged. Useful when diffing decompiler output
across rebuilds of obfuscated assemblies, where IL block layout is
unstable but the case-to-value mapping is not.

Includes an ILPretty test that exercises a hand-written switch whose
table targets are placed at non-monotonic IL offsets (simulating
obfuscator block shuffling) and verifies the cases come out in
label-value order with the setting enabled. Also adds the
Resources.resx / Resources.Designer.cs entry so the WPF settings UI
shows a proper label instead of the raw key.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-10 20:08:51 +02:00
Siegfried Pammer f6c763e7bb Add tests for ExtensionEncodingV1 and ExtensionEncodingV2. 2025-11-02 08:49:59 +01:00
Jeremy Pritts d1dc7b258a Copy-propagate the stack slot for collection initializers (#3554)
* Copy-propagate the stack slot for collection initializers

* Use pattern matching
2025-09-10 08:18:17 +02:00
Peter Crabtree e1e16b64f5 dev: Strip BOM mark from text files 2025-08-28 20:56:19 -04:00
Daniel Grunwald 317b4e8add Merge branch 'fix/3465' 2025-08-23 18:02:44 +02:00
Daniel Grunwald 89083eaf48 Fix #3344: Add support for the ckfinite opcode. 2025-08-23 15:52:04 +02:00
Siegfried Pammer 052466fce7 Add test case 2025-08-15 00:04:42 +02:00
Siegfried Pammer ddb7171a2a Fix #3524: Property without backing field cannot have an initializer. 2025-07-30 07:28:45 +02:00
Siegfried Pammer cc4588db4b Add a test case 2025-07-05 19:06:46 +02:00
Jeremy Pritts 0261ca3ae8 Fix stack overflow exception in CSharpResolver (#3471)
* Fix stack overflow exception in CSharpResolver

* lhs and rhs
2025-05-17 08:31:50 +02:00
ds5678 7f024de786 Don't include generic constraints in generated explicit overrides 2025-03-30 14:16:43 -07:00
ds5678 a1b3b14b0b Add test 2025-03-15 00:51:53 -07:00
Christoph Wille fba0dbae8d Migrate to NUnit 4 (#3132)
* Migrate to NUnit 4 based on https://docs.nunit.org/articles/nunit/release-notes/Nunit4.0-MigrationGuide.html. Needed fix for https://github.com/nunit/nunit/issues/4564
* Test 4.1.0-adev.1 from NUnit MyGet feed (works locally in VS)
* Update to bugfix version adev.2 (works on GH)
* NUnit 4.0.1 final
2023-12-02 13:38:55 +01:00
ElektroKill 4601481dec Add ILPretty test for MCS 2.6.4 fixed string 2023-06-19 10:50:07 +02:00
Daniel Grunwald 94d2ce0fcf Fix crash when control flow reaches end of method. 2023-06-01 14:34:05 +02:00
Siegfried Pammer 6139d9d58e Delete created temp files if test execution was successful. 2022-08-24 22:24:51 +02:00
James May 4a1f79a32a Assume conventionally named unresolved method references are properties or events.
eg. decompiles to .Property instead of .get_Property()
2022-04-27 15:35:02 +10:00
Siegfried Pammer e4b63f7a35 - Make all tests async.
- Use custom test runner for correctness tests.
- Use .NET 6.0 framework for all Roslyn compiler tests.
- Disabled tests involving new string interpolation patterns.
2022-03-07 09:05:08 +01:00
Siegfried Pammer d8bb40b45b Generate file-scoped namespace declarations, if possible. 2021-11-24 10:58:28 +01:00
Siegfried Pammer ec1b9a4ccd Fix #2443: do not use this()-ctor initializers by accident if base type is unknown. 2021-07-01 18:56:34 +02:00
Daniel Grunwald 6951ccb1a2 Fix #2260: switch(string) transform: handle empty cases where the C# compiler optimizes out the if
e.g.
```
	switch (<PrivateImplementationDetails>.ComputeStringHash(text2))
	{
	case 1288728352u:
		_ = text2 == "rowno";
		break;
        ...
```
2021-01-23 16:55:51 +01:00
Daniel Grunwald 2683079db6 Merge remote-tracking branch 'origin/release/6.2' into master 2020-10-12 17:01:53 +02:00
Daniel Grunwald 80cf72e498 #2182: Move "Transform call to struct constructor" from EarlyExpressionTransforms to ILReader.
This fixes the evaluation order for such calls (and an assertion in StObj.CheckTargetSlot).
2020-10-07 23:59:22 +02:00
Daniel Grunwald 47685c4b57 Fix weird casts to nint when writing to fields of unknown type. 2020-10-07 20:47:26 +02:00
Daniel Grunwald 836bc37050 Upgrade ilasm.exe version. This allows us to write tests for the new function pointer calling conventions. 2020-09-30 00:08:11 +02:00
dotnet format 0d9f871a4f #2128: Reformat the whole code base. 2020-08-28 01:05:38 +02:00
Siegfried Pammer 9e0e8d1cbc Add basic support for C# 8.0 enhanced using statements. 2020-08-23 16:42:30 +02:00
Siegfried Pammer 15028b3669 Fix #2104: Check field name of backing field when attempting to transform to an auto property. 2020-08-23 09:14:27 +02:00
Daniel Grunwald abb9d49a0f Add support for C# 8 switch expressions. 2020-08-02 11:57:33 +02:00
Daniel Grunwald 2c0554f129 Fix #1963: Support decompiling calli instructions into C# 9 function pointer syntax. 2020-06-24 02:28:28 +02:00
Daniel Grunwald 65fe59e393 #1918: Fix a bunch of issues with pinned region detection.
Not every pinned region has a clean `P = null` assignment to mark its end.
If a second pinned region starts with the same variable `P`, consider that to mark the end of the previous pinned region for that variable.

Also, fix a bunch of special cases with empty pinned regions.
2020-04-03 22:57:12 +02:00