Commit Graph
1515 Commits
Author SHA1 Message Date
Siegfried Pammer 0fbea35cfd Add pretty tests for C# 12 primary constructors on non-record types
The decompiler already reconstructs primary constructor syntax for most
non-record shapes (UsePrimaryConstructorSyntaxForNonRecordTypes, C# 12);
Issue3598.cs pins the ctor-vs-primary-ctor decision matrix. The new
green PrimaryConstructors fixture pins the remaining shapes: parameter
capture in methods, iterators, async methods, lambdas, and local
functions; double use of a parameter in a field initializer and a
method body (CS9124); a parameter both passed to the base constructor
and captured (CS9107); mutation of a captured parameter in classes and
structs; parameter-member shadowing in both directions; generic types;
readonly structs; in parameters; attributes on primary constructor
parameters; unsafe pointer parameters; statics and nested types beside
a captured parameter. An empty primary constructor prints as a plain
class declaration (equivalent), pinned via EXPECTED_OUTPUT.

PrimaryConstructorAttributes is Assert.Ignore'd: attributes applied to
the primary constructor via the [method:] target are present on the
.ctor in metadata but silently dropped from the decompiled output for
both classes and structs, losing semantics (e.g. [Obsolete]). The
fixture is the desired output; all four Roslyn 4.14/latest debug/opt
configs compile it and fail only at the output comparison.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-29 07:19:30 +02:00
Siegfried Pammer d7bbf17e2e Add pretty test for C# 13 ref struct interfaces / allows ref struct
Covers ref structs implementing interfaces (implicit, explicit, and
default-interface-method reimplementation, which CS9245 forces on every
ref struct implementer), the allows ref struct anti-constraint on
methods, classes, interfaces, delegates, local functions, iterators,
async methods, and capturing local functions, constraint combinations
(interface, IDisposable with using, unmanaged, struct, new()),
interface members invoked through a constrained T (instance, static
abstract factory, and default interface methods, which are callable
through T because implementers must always override them), scoped/ref/
in/out parameters of T, and call sites instantiating with Span/
ReadOnlySpan type arguments.

The test is green in all four Roslyn 4.14/latest debug/opt configs: the
decompiler already round-trips the gpAcceptByRefLike flag into
"allows ref struct" clauses and re-emits ref struct interface
implementations correctly. One cosmetic quirk is pinned as-is: calls
whose type arguments are ref structs are printed with explicit type
arguments and a declaring-type qualifier even within the declaring
class, because type-argument inference validation does not accept ref
struct type arguments; the output remains compilable and semantically
identical.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-29 06:23:05 +02:00
Siegfried Pammer 800efc6e10 Fix #749: toggle a member's fold together with its documentation
Toggle folding picked the innermost fold containing the offset, but a
member's logical region is fragmented: the body fold starts at the
opening brace, the XML documentation has an independent fold, and the
header line belongs to neither, so toggling there collapsed the whole
enclosing type. Visual Studio's source editor keeps documentation
regions independent, but its metadata-as-source view treats the
member's leading trivia as one hideable unit; for a read-only
decompiler view the grouped behavior is the intuitive one.

The writer now records where an entity declaration begins, and the
definition's fold carries that logical start. Toggling targets the fold
whose logical region innermost-contains the offset, so the header line
targets the member rather than the type, and leading documentation
folds follow the member fold's new state. Inside the documentation the
doc fold itself is the innermost region and still toggles alone.

Toggle all folding now follows Visual Studio's Toggle All Outlining
parity: a mixed state expands everything, a uniform state flips.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-28 16:15:42 +02:00
Siegfried Pammer 486ff7fccb Merge pull request #3910 from sailro/fix-3909-default-constraint
Fix #3909: emit nullable override disambiguators
2026-07-27 18:26:02 +02:00
Siegfried Pammer ecc1583633 Make the netcore-2.2 reference set work with vbc 2.10
The netcore-2.2 reference set consists of the shared framework's facade
assemblies split across many files, and vbc only binds special types
like System.Void from an assembly that defines them rather than
following type forwards, so without an implicit SDK it needs the same
reference list as the C# side. The VB runtime must come from the legacy
reference set: before .NET Core 3.0 there is no
Microsoft.VisualBasic.Core.dll and the core build of the VB runtime is
a trimmed-down subset (no UBound etc.). Referencing the target
framework's own Microsoft.VisualBasic facade alongside that -vbruntime
choice is a BC32210 identity conflict, so it is dropped from both the
default reference list and the ReferenceVisualBasic flag handling.
All of this applies only where vbc runs without its implicit desktop
SDK path, i.e. off Windows; on Windows vbc.exe keeps the plain
reference list that already worked.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 18:25:10 +02:00
Siegfried Pammer 6ab0a48b0f Support Roslyn 1.x and 2.x compilers in tests on non-Windows
The Roslyn 1.3.2 and 2.10.0 configurations were excluded from the
compiler matrix on non-Windows platforms because Microsoft.Net.Compilers
only ships .NET Framework executables. Both can be enabled:

- Roslyn 2.10.0 has a dotnet-hosted sibling package,
  Microsoft.NETCore.Compilers, whose tools/bincore/csc.dll runs on the
  installed runtime with --roll-forward LatestMajor (its runtimeconfig
  pins the out-of-support .NET Core 2.0). Fetched on non-Windows into
  the version's tools/bincore directory; GetCSharpCompiler probes for a
  direct csc.dll next to the installed path in addition to the
  bincore/ subfolder layout of the newer toolset packages.
- Roslyn 1.3.2 has no .NET build; when a mono executable is found on
  the PATH, it is kept in the matrix and WrapCompiler hosts the .exe
  compilers through mono. Because the native DiaSymReader needed for
  Windows PDBs is unavailable there, GeneratePdb requests portable
  PDBs on non-Windows (Roslyn 2.x+ falls back on its own, 1.x needs
  the explicit -debug:portable).

The mcs configurations stay excluded: the bundled mcs 2.6.4 needs the
Reflection.Emit COMPILER_ACCESS mode that current Mono runtimes no
longer implement. Old-compiler configurations also stay excluded from
correctness-style fixtures: their output targets .NET Framework or
.NET Core 2.2, which the runners cannot execute here.
Microsoft.NETCore.Compilers-2.10.0.nupkg should be added to
ILSpy-tests/nuget to keep the fetch offline-capable.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 18:25:10 +02:00
Siegfried Pammer ffbd2055b0 Pin disambiguators for constraints on sibling method type parameters
The chain cases in the fixture route T : TOuter through a class-level
type parameter; the variant where the dependency target is a sibling
method type parameter (M<T, U> with T : U) was uncovered. It pins the
same alignment from a different angle: csc rejects 'class' with CS8665
when U is merely class-constrained and requires 'default', but accepts
'class' when U carries a class-type constraint, matching what the
tri-state IsReferenceType derives. Both directions were verified
against csc before adding the expected output.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:45:36 +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 4c04e01882 Cover virtual, overloaded, and renamed-Implements parameterized properties
The renamed-Implements case exposed a gap: the accessor-method
declarations did not include the explicit-interface-implementation
forwarders generated from .override directives, so decompiled types did
not implement their interfaces and same-name implementations lost their
interface mapping. DecompileParameterizedProperty now emits the same
forwarder stubs as the ordinary method path.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:28:34 +02:00
Siegfried Pammer 7865f9b23e Restore parameter lists of parameterized properties in signatures
The main tree, tooltips, and search results once showed the parameter
list of a parameterized property; the ambience lost that when property
rendering went through the converted AST node, whose C# property syntax
cannot carry parameters. Take the parameter list from the symbol
instead and render it in parentheses (matching VB.NET usage syntax and
distinguishing these properties from indexers).

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:28:34 +02:00
Siegfried Pammer 832090c08d Emit parameterized-property accessors as ordinary methods
C# cannot declare a named property with parameters: only the type's
default member gets indexer syntax, and reusing it via [IndexerName]
collapses for types with several differently-named indexed properties,
static properties, or explicit interface implementations. Emitting the
accessors as ordinary methods is the only fully general compilable
form, matches how C# consumes such properties (Roslyn exposes the
accessors of properties it cannot bind as regular methods, the same
pattern C# 14 made user-facing for extension-member disambiguation),
and round-trips call sites to identical IL. Call sites already lower
to direct accessor calls.

The property-level attributes are kept on the first accessor under the
'property:' attribute target: it is not valid on methods, so csc emits
nothing for it (CS0657 warning) and recompilation neither loses the
attributes from the source nor misapplies them to the accessor. A
comment on the first accessor documents the deliberate deviation.
Visual Studio's metadata-as-source view drops such properties'
attributes entirely.

The assembly tree and tooltips are unaffected: they keep rendering the
property node with its parameter list. Known limitation, inherent to
any C# projection: recompiling the output produces plain methods, so
VB.NET consumers of the recompiled assembly lose property syntax.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:28:34 +02:00
Siegfried Pammer 4783fe1d34 Add VBPretty spec for VB.NET parameterized properties
C# has no syntax for named properties with parameters, so the expected
output declares the accessors as ordinary methods, keeps the
property-level attributes under the inert 'property:' attribute target
(ignored by csc with CS0657), and consumes the properties through
direct accessor calls, which Roslyn permits exactly for properties
whose shape C# cannot bind. Red against the current decompiler, which
emits a parameterless property whose body references the vanished
parameters.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:28:34 +02:00
Siegfried Pammer 9bff4ad956 Fix #1603: recognize foreach over enumerators that cannot be disposable
The foreach pattern was only matched against using instructions, but
the compiler emits no using/try-finally at all when the enumerator's
static type can never require disposal: a struct or a sealed class
that does not implement IDisposable (SerializationInfoEnumerator in
the issue's example). Such loops stayed while loops.

Recognize the bare 'enumerator = x.GetEnumerator(); while
(enumerator.MoveNext())' shape during statement building and reuse the
existing foreach transformation core for it. The transformation is
restricted to exactly the cases where recompilation would produce the
same IL: the enumerator type rules above (ref structs are excluded
because of pattern-based disposal), a single-store enumerator variable
unused outside the loop, and synchronous enumeration only, since async
enumerators are always IAsyncDisposable.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 07:38:22 +02:00
Siegfried Pammer 132b33e530 Cover the C# standard type-inference rules in TypeInferenceTests
Walks the inference algorithm of the standard (draft-v11, 12.6.3) and
adds a test per rule that the revived NRefactory suite did not already
exercise: exact inference for ref parameters and its non-applicability
of the base-type walk, explicit lambda parameter types, exact/upper
bound inference through arrays, nullables and variance nesting, the
unique-base-type restriction, value-type elements forcing exact
inference, conflicting exact bounds, and best common type.

Two rules are pinned as ignored tests because the implementation does
not follow the standard yet: a value argument to an 'in' parameter
infers no bound (12.6.3.7 wants a lower-bound inference), and tuple
literals are not inferred elementwise (12.6.3.7/12.6.3.8). Both tests
assert the csc-verified result and should go green when the rules are
implemented.

Nullable unwrapping in exact and upper-bound inferences needs no
dedicated code path in this implementation: T? is represented as the
constructed type Nullable<T>, so the constructed-type case already
produces the elementwise exact inference the standard asks for; the
new tests pin that equivalence. Function-pointer inference rules and
the explicit-return-type inference of 12.6.3.15 remain untested: the
former needs a MetadataModule to construct FunctionPointerType, the
latter is not representable in LambdaResolveResult.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 06:52:08 +02:00
Siegfried Pammer e7f42d1b3c Revive TypeInferenceTests from NRefactory
The direct unit tests for TypeInference were lost when the NRefactory
sources were replaced by the NuGet package (e88120cb4); since then the
class had no dedicated coverage and ConversionTests still pointed to a
test that no longer existed. Ported to the current type system API and
NUnit constraint asserts. The two tests NRefactory ignored on .NET 4.5
now pin the covariant IReadOnlyList<T> results, since the test
compilation uses the 4.5-era reference mscorlib; the common-subtype list
test gains the ReadOnlyCollectionBuilder<T> candidates contributed by
System.Core, which the NRefactory compilation did not reference.

Also includes the seven tests that only exist in upstream
icsharpcode/NRefactory (async lambdas, NullablePick, CoContraPick,
bug 9300, user-defined-conversion bounds). Upstream wrote them against
its source-based resolver harness, which this repo does not have, so
they are reexpressed as direct InferTypeArguments calls using mock
lambdas and helper types declared in the test assembly. Upstream's
InferFromImplicitAsyncLambda was missing its [Test] attribute and never
actually ran; here it does.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 06:52:08 +02:00
Sebastien Lebreton ee2d747862 Pin the allows ref struct interaction on nullable overrides
allows ref struct is inherited implicitly, so restating it on an override is
CS0460 even alongside a legal disambiguator. Roslyn still re-emits the byreflike
flag on the override's own type parameter, and the general constraint printer
turns that flag back into source, so the disambiguator stays legal only as long
as it is built separately. Cover a C# 13 base whose annotated and plain methods
both allow ref structs.

Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
2026-07-26 19:28:59 +02:00
Sebastien Lebreton 0e600382b0 Classify nullable override disambiguators by reference type
A class-type constraint such as Stream or Delegate sets no
ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave
those overrides the default constraint, which is CS8822, and the output still
did not recompile. The restated disambiguator leaves no metadata trace of its
own, so the choice has to follow from whether the inherited constraints make
the type parameter a reference type, a value type, or neither.

Matching the annotated type parameters by identity rather than by owner kind
and index also keeps a specialized signature from contributing a foreign type
parameter that happens to share an index.

Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
2026-07-25 16:37:35 +02:00
Siegfried Pammer 3dc3f8446b Merge pull request #3911 from sailro/fix-3908-constrained-async-yield
Fix #3908: use the async iterator element type for yield
2026-07-25 08:24:38 +02:00
Siegfried Pammer 6932586827 Document two CSharpConversions bugs found by differential fuzzing csc
Dumping the implicit/explicit classification of all pairs from a 120-type
universe and diffing it against what csc actually compiles surfaced two
divergences (16 affected pairs), both proven by a compiling-and-running
snippet and captured here as ignored known-bug tests:

1. Nullable conversions derived from tuple conversions are missing:
   csc accepts "(long, object)? a = t;" for t of type (int, string) as
   well as the lifted and explicit forms, CSharpConversions returns None.
   The ECMA spec's 10.6.1 does not list tuple conversions as liftable,
   so this is a case of Roslyn exceeding the spec.
2. An explicit user-defined conversion to a nullable target is rejected
   when the operator result additionally needs an explicit numeric
   conversion: csc accepts "(int?)new ImplicitToLong()" (operator to
   long, then explicit long -> int?), CSharpConversions returns None.

The same sweep confirmed the SByte..Decimal TypeCode range in
ImplicitEnumerationConversion is correct: csc accepts zero constants of
any numeric type (0.0, 0f, 0m) for enum conversion and rejects '\0',
matching the implementation exactly.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-25 08:09:14 +02:00
Siegfried Pammer 787cc71b3a Cover every rule of C# standard chapter 10 in the conversion tests
Audit the conversion test suites against the conversions chapter of the
draft-v8 C# standard and add tests for every rule that had none:
exhaustive implicit/explicit numeric conversion matrix, tuple/ValueTuple
and nullable-annotation identity, interpolated-string / throw-expression /
tuple-literal conversions, the boxing rule set including variance-based
boxing and unboxing, delegate-to-System.Delegate and IReadOnlyList<T>
reference conversions, type-parameter variance and effective-base-class
casts, generic method groups (inference, explicit type arguments, no
inference from the return type), the anonymous-function compatibility
checks CSharpConversions performs itself (via a LambdaResolveResult test
double), standard-conversion exclusion of user-defined operators, and
operators declared in base classes of the source type.

Two rules are implementation gaps rather than test gaps and get ignored
placeholder tests naming the gap: default literal conversions (10.2.16)
are an explicit TODO in CSharpConversions, and switch expressions
(10.2.18) have no ResolveResult representation. The full
section-by-section map is in
Analyses/ILSpy/2026-07-24_conversions-spec-coverage.md.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-25 08:09:14 +02:00
Siegfried Pammer 729fe03c29 Revive NRefactory explicit-conversion tests without resolver/AST
Convert the commented-out block in ExplicitConversionsTest the same way as
the implicit ConversionTest block: type-parameter casts via
DefaultTypeParameter with cross-referencing constraints, user-defined
operators as fixture types in the test assembly, constant sources via
ConstantResolveResult. The rr.Input asserts of the originals were resolver
artifacts and are dropped; UseDefinedExplicitConversion_Lifted instead
exercises the ResolveResult-based ExplicitConversion entry point.

Also extend PreferAmbiguousConversionOverReferenceConversion with the
overload-resolution half of the original NRefactory test (the ambiguous
conversion must not prevent M(BB) from being chosen over M(object)), which
the first revival pass had reduced to the conversion classification alone.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-25 08:09:14 +02:00
Siegfried Pammer ce7883fe22 Revive NRefactory conversion tests using ResolveResult/IType fixtures
The block of tests inherited from NRefactory's ConversionsTest was
commented out because it depended on ResolverTestBase (full AST +
CSharpResolver). Rewrite all of them against CSharpConversions directly:
hand-built MethodGroupResolveResults over fixture types compiled into the
test assembly (extension methods injected via the internal
extensionMethods field), DefaultTypeParameter instances with
cross-referencing constraints, and metadata-backed fixture interfaces for
the ExpansiveInheritance termination test.

Two deviations from the NRefactory originals:
MethodGroupConversion_RefArgumentObjectVsDynamic now expects a valid
conversion, because object/dynamic mismatch in a ref parameter is an
identity conversion and current csc accepts the assignment (verified);
PreferUserDefinedConversionOverReferenceConversion resolved an invocation,
so it is recast as an OverloadResolution test over FakeMethod candidates.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-25 08:09:14 +02:00
Sebastien LebretonandCopilot 008cf7a5d1 Fix #3909: emit nullable override disambiguators
Override constraints are normally inherited and omitted, but nullable type
parameters still require class or default to distinguish annotations from
Nullable<T>. Derive that legal discriminator from the method metadata.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-07-24 18:10:33 +02:00
Sebastien LebretonandCopilot eb6d341866 Fix #3908: use the async iterator element type for yield
Yield translation derived its target type only from synchronous enumerable
interfaces, leaving async iterator yields untyped and preserving compiler
boxing casts. Use the element type already recovered by the async decompiler.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-07-24 18:10:21 +02:00
Siegfried Pammer 753f1d2481 Fix #1924: don't undo the compiler's &&-to-& optimization
ExpressionBuilder printed bitwise & / | on booleans as && / || whenever
the right-hand side was pure, but Roslyn lowers && / || to & / | only
when the right operand is a bare local or parameter read
(LocalRewriter.MakeBinaryOperator, unchanged since 2014). Shapes like
(c == 'a') | (c == 'b') can therefore only originate from a bitwise
source operator, yet were shown as short-circuiting. Per the discussion
in #1545, show the operator the IL actually uses instead of guessing the
source form: the reversal is dropped entirely, so Roslyn-compiled
"a && b" now decompiles to "a & b", which recompiles to the same IL.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 18:51:52 +02:00
Siegfried Pammer 3a6b074930 Merge pull request #3896 from sailro/fix-3894-span-concat
Fix #3895: keep byref-like ToString in string concatenation
2026-07-23 17:03:47 +02:00
Siegfried Pammer a926ae7e8b Show a hover for the indexer of a dynamic index access
A dynamic index access (a[b]) gave its IndexerExpression a
DynamicInvocationResolveResult with no symbol, so the brackets carried no
tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target
type with the index parameters typed from the callsite delegate, and
attach it. Route it hover-only by detecting a DynamicInvocationResolveResult
directly on the node - which also covers an invoke-member's own
parentheses, so those stop producing a dead navigation link too.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +02:00
Siegfried Pammer cba8b0d7a5 Test dynamic member tooltip rendering
Lock the hover content the dynamic-tooltip work produces. Ambience-level
cases (CSharpAmbienceTests) pin that SpecialType.Dynamic renders as
"dynamic" and that a synthetic dynamic method renders its return and
per-argument types - including the full hover form, confirming the
unnamed synthetic parameters collapse to their types with no dangling
name. An end-to-end case (HoverOnlyReferenceTests) decompiles a dynamic
call and renders the symbol GetSymbol hands back, exercising the actual
synthesis (argument typing from the callsite delegate), not a hand-built
stand-in.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +02:00
Siegfried Pammer 4ff11dd796 Add a hover-only reference mode, distinct from local highlighting
References had two behaviors encoded in one bool: navigable links, and
"local" references that are non-navigable but highlight all occurrences
on click. Synthesized dynamic members were routed through the latter, so
they picked up the occurrence highlight even though each use is a
distinct synthetic member with nothing to group.

Model the three modes explicitly with a ReferenceMode enum on
ReferenceSegment (Link / LocalHighlight / HoverOnly). Dynamic members,
the dynamic constructor and the dynamic keyword now use HoverOnly: they
still show a hover tooltip (BuildHoverContent resolves any IEntity
reference regardless of mode) but get the arrow cursor, do not navigate,
and do not highlight. Local variables keep LocalHighlight. Plumbed via a
new isHoverOnly flag on ITextOutput.WriteLocalReference.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +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
Christoph Wille 2e617a38c0 Report deduplicated reference count, deflake EventSource schema tests
TypeSystemInitStop now reports the size of the final reference set passed
to Init() instead of the raw resolve count, which included same-name
duplicates that the version dedup later drops. The schema smoke tests
asserted exact global event counts, but the providers are process-wide
and the decompiler fixtures run in parallel, so unrelated decompilations
could inflate the counts; every string payload now carries a unique
marker (and the string-less snapshot events a sentinel count) that the
assertions filter on.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-22 21:10:41 +02:00
Christoph Wille c8474419c4 Redesign DecompilerEventSource with Start/Stop tracing and keyword gating
The events introduced in #2519 timed only the five per-entity DoDecompile
overloads, allocated a Stopwatch and the member's FullName even when no
trace session was attached, and reported whole milliseconds, which rounds
almost every member to zero. Flat one-shot events also gave PerfView and
dotnet-trace no way to show durations or nesting, and the actually
expensive stages (type system initialization, assembly resolution probing,
the IL/AST transform pipelines, whole-project decompilation) were not
instrumented at all.

Start/Stop event pairs let trace viewers derive duration and nesting from
event timestamps, keywords let a session enable only the areas of
interest, and every call site is gated on IsEnabled() so tracing costs a
branch when disabled. Per-transform events are Verbose because of their
volume; unlike the STEP/Stepper mechanism they work in Release builds.
EventSource is in-box for netstandard2.0 and flows over both ETW and
EventPipe, so this stays cross-platform with no new dependency.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-22 21:10:41 +02:00
Siegfried Pammer fddd62286b Beautify enum declarations by referencing previously declared members
Enum members whose value duplicates an earlier member now reference it
(Item2B = Item2A), [Flags] members combine earlier single-bit members
(All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1)
instead of showing a bare number.

Several guardrails keep the output faithful to how such enums are
written by hand: only previously declared members are referenced (field
row order); a multi-bit value lying entirely within a larger, earlier
member is a field encoding inside that mask, not a flag union, and
stays numeric, as do zero-valued members of [Flags] enums, which
routinely have several unrelated zero members. The ~X form is
suppressed in byte/ushort enum declarations, where the initializer
constant folds in int and would not compile. Enums with unusual
underlying types (bool, native int) keep the plain constant conversion.
With these rules, decompiling System.Private.CoreLib reproduces the
hand-written declarations of TypeAttributes, MethodAttributes,
AttributeTargets and FileAttributes almost verbatim.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-22 18:03:33 +02:00
Sebastien LebretonandCopilot 675fca83a8 Fix #3895: keep byref-like ToString in string concatenation
String concatenation cannot box a byref-like value, so removing its explicit
ToString call produces invalid C#. Preserve that call while retaining the
surrounding string-concatenation reconstruction.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-07-20 01:02:38 +02:00
Sebastien LebretonandCopilot ad991548b0 Fix #3892: preserve unsafe for pointer constructors
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-07-20 00:30:04 +02:00
Siegfried Pammer 7554c1b004 Remove the syntactic automatic-event patterns
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.

Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
2026-07-18 21:56:16 +02:00
Siegfried Pammer 461df88572 Replace automatic-event backing-field references during translation
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.

mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.

Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
2026-07-18 21:56:16 +02:00
Siegfried Pammer 198639be0d Emit the backing field when a name-associated event is not automatic
MemberIsHidden decides field hiding from the metadata name association
alone, which over-approximates: an event whose accessors fail the ILAst
validation is decompiled with explicit accessors, and while a referenced
backing field is re-added through the work list, an unreferenced one was
silently dropped from the output. The type-definition member loop now
consults the same memoized verdict as the event declaration, so the two
decisions agree by construction.

Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
2026-07-18 21:56:16 +02:00
Siegfried Pammer 1b9131905d Fix #3858: Detect automatic events on the ILAst instead of the C# AST
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).

Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.

mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-18 21:56:16 +02:00
Christoph Wille b888153f24 NuGet 07/26 updates and .NET 11 P6 Roslyn 2026-07-14 20:46:08 +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
ds5678 898c2a3f9f Add Issue3877 test and handle negative dict capacity
Added Issue3877 test to PrettyTestRunner and new test case source to verify dictionary initialization with negative capacity. Updated SwitchOnStringTransform to skip processing when a negative dictionary capacity is detected.
2026-07-11 13:54:32 -07:00
Siegfried Pammer 17e6a63c2b Fix #3475: Emit 'true ? null : new { ... }' for null of anonymous type
When a null literal is the only argument a generic type argument could
be inferred from, and that type argument is an anonymous type, ILSpy
used to drop the type arguments entirely (they cannot be written
explicitly), producing 'Test(null)', which no longer compiles
(CS0411). A conditional expression whose never-taken branch creates an
instance of the anonymous type is the minimal C# expression that gives
a null value that type, so the argument now carries enough information
for type inference.

The instance expression is obtained by translating a synthesized
'newobj' with 'default.value' arguments through the existing pipeline
rather than assembling syntax by hand; the property values are typed
defaults, since the IL only contains ldnull. Anonymous types occurring
inside other constructed types (e.g. arrays) stay unexpressible and
keep the previous output.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-11 13:24:37 +02:00
Siegfried Pammer 5a6e1515eb Add test for out-var reference nested within another argument
CS8196 also fires when the second reference sits inside a nested call
in another argument of the declaring call, e.g.
OutAndValue(out var a, UseAndReturn(out a)). The existing check already
covers this because it walks all descendants of the sibling arguments;
this fixture pins that behavior.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-11 13:24:18 +02:00
Siegfried Pammer 1e6a19e2c4 Fix #3860: Avoid 'out var' if the variable recurs in the argument list
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-11 13:24:18 +02:00
Siegfried Pammer c52a0f29f6 Fix #1388, #1928: decompile await on dynamic expressions
Awaiting a dynamic value lowers GetAwaiter/IsCompleted/GetResult to
dynamic callsites, which async decompilation could not recognize:
await detection ran before DynamicCallSiteTransform, so the await was
emitted as a raw state machine (or crashed in AnalyzeAwaitBlock, #1388).

AnalyzeStateMachine now collapses the awaiter callsites per block, folds
the runtime ICriticalNotifyCompletion branch the compiler emits for an
awaiter not statically known to implement it into the canonical single
call, and re-joins the branch chains the collapse leaves so each dynamic
await sits in one block. DetectAwaitPattern matches the dynamic
GetAwaiter/IsCompleted/GetResult shape and emits `await expr`; a
synthesized dynamic GetResult method gives the await and its local the
dynamic type. DynamicCallSiteTransform also follows callsite targets
spilled into state-machine locals, so an awaited value flowing into a
dynamic callsite (e.g. d.Result = await ..., #1928) decompiles too.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-11 13:23:45 +02:00
Siegfried Pammer 322add8d14 Fix dynamic event-assignment decompilation leaking is-event opcode
DynamicIsEventInstruction has no ExpressionBuilder visitor, so any is-event
diamond that survives the transform pipeline leaks as an "OpCode not supported"
comment. The collapse only fired for the plain statement form; every other
lowering the compiler emits for "d.Event += b" was missed:

- a copy-of-value temporary between the getmember cache and the diamond (modern
  Roslyn emits it for the statement form),
- the result-used shape, where the diamond is a value-if nested inside the
  consuming expression (a call argument, or a leave's value when returned),
- the result-returned shape as two leaves, "if (isevent) leave(add)" plus a
  fall-through "leave(compound)" rather than an if/else, and
- the same two-leaves shape when the optimizer drops the getmember cache (legacy
  csc and Roslyn <= 2.x): the is-event flag then feeds a single branch, inlining
  folds it into the condition, and the compound leave is already collapsed, so
  the cache-based entry never matched it and the branch leaked. These compilers
  only run on Windows CI, so the gap was invisible on Linux.

The descendants walk handles the nested value-if once an over-strict
copy-of-value bounds guard (which never held for the compact return block) is
dropped. The two two-leaves shapes share a skeleton matcher and the add/remove
accessor-name check, and both validate the accessor name and arguments. Re-running
from the collapsed statement instead of the next one avoids indexing past the end
of a block that has shrunk to a single leave.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-11 12:39:03 +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 52e2161ea7 Add regression test for duplicate array-element assignment
A second store to an already-written array element must prevent
HandleSimpleArrayInitializer from folding the stores into a collection
initializer: the earlier store would be dropped (a visible change when
its value has side effects), and without the nextMinimumIndex guard the
transform crashes on the colliding index. The nextMinimumIndex check
already enforces this; pin it with a Pretty fixture so the boundary
between a foldable initializer and a duplicate-write sequence stays
covered.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-07 09:52:17 +02:00