The order of a record's fields and properties has to be known, because
Equals, GetHashCode, PrintMembers and the copy constructor are recognised
by walking their bodies in lockstep with it. It was assumed to be every
property followed by every field, so a record that declares a field
before a property desynchronised all four at once: none was recognised as
generated, all of them were emitted, and the auto-properties lost their
backing fields to raw <Property>k__BackingField accesses - output that
does not compile.
The order is in the generated members themselves, but no single one has
all of it: Equals compares everything that carries state and never a
computed property, PrintMembers prints everything public and never a
private field. Both follow declaration order, so the two sequences are
merged along the members they share, which puts a private field and a
computed property back in the right places relative to each other.
Members neither of them mentions - EqualityContract, static members -
keep the position they had.
Where the two orders conflict, which can only happen for a member that
one of them never sees, the equality order wins; nothing in the metadata
says more, and the choice cannot change more than the order the members
are printed in.
Assisted-by: Claude:claude-opus-5:Claude Code
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
Structural recognition of compiler-generated code needs method bodies as
ILAst decompiled with a fixed set of settings, so that recognition does
not depend on user-visible options. RecordDecompiler had this pipeline
as a private helper; hoist it to CSharpDecompiler so other recognizers
can share it, deriving the generic context from the method's declaring
type instead of a captured type definition.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
Various improvements regarding primary constructor decompilation, including:
- introduce `HasPrimaryConstructor` property in the AST, as there is a difference between no primary constructor and a parameterless primary constructor
- improved support for inherited records and forwarded ctor calls
- exclude non-public fields and properties in IsPrintedMember
- introduce an option to always make the decompiler emit primary constructors, when possible
This commit updates `RecordDecompiler` to avoid a null ref when the
decompiler is unable to determine the base type of a record (e.g.
because the base type is defined in another assembly that is not
loaded).