Add pretty test for C# 10 sealed record ToString

Records may declare a sealed override of ToString (csharplang#4174),
which changes what the compiler synthesizes in derived records: no
ToString override is generated when a base record sealed it. The suite
pins that a user-written sealed ToString survives decompilation with
its sealed modifier (including one whose body mimics the generated
ToString shape, where only the sealed flag distinguishes it), that
records deriving from such a base (with and without new members,
generic, and below an abstract root) still get their synthesized
PrintMembers machinery hidden, and that non-sealed custom ToString,
custom PrintMembers next to a sealed ToString, sealed record types,
and record structs with a custom ToString all round-trip. A user
PrintMembers that is IL-equivalent to the generated one is folded into
the synthesized members and not printed; this is pinned via an
EXPECTED_OUTPUT guard since recompilation regenerates an identical
method. All cases are green under roslyn4OrNewerOptions.

The sealed modifier is illegal on record struct ToString (CS0106), so
there is no struct sealed case. A pre-existing, unrelated defect was
observed while probing: a generic record deriving from a generic record
base with primary-constructor base arguments decompiles to invalid
"base..ctor(...)" output and leaks Deconstruct; it reproduces without
any custom ToString and is therefore not part of this suite.

Assisted-by: Claude:claude-fable-5:Claude Code
This commit is contained in:
Siegfried Pammer
2026-07-29 07:19:30 +02:00
parent d7bbf17e2e
commit e449a54b84
2 changed files with 125 additions and 0 deletions
@@ -651,6 +651,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task SealedRecordToString([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task Issue3610([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
@@ -0,0 +1,119 @@
using System.Text;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class SealedRecordToString
{
public abstract record AbstractBase(int X);
public record Middle(int X, int Y) : AbstractBase(X)
{
public sealed override string ToString()
{
return "middle";
}
}
public record Leaf(int X, int Y, int Z) : Middle(X, Y);
public record WithSealedToString(int A)
{
public sealed override string ToString()
{
return "custom";
}
}
public record DerivedAddsMembers(int A, string B) : WithSealedToString(A);
public record DerivedAddsNoMembers(int A) : WithSealedToString(A);
public record GenericDerived<T>(int A, T Extra) : WithSealedToString(A);
public record WithNonSealedToString(int A)
{
public override string ToString()
{
return "custom";
}
}
public record DerivedFromNonSealed(int A, string B) : WithNonSealedToString(A);
public record WithSealedToStringAndPrintMembers(int A)
{
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append("CustomA = ");
builder.Append(A);
return true;
}
public sealed override string ToString()
{
return "custom";
}
}
public record EquivalentPrintMembers(int A)
{
// A user-written PrintMembers whose body is IL-equivalent to the
// compiler-generated one is folded into the synthesized record members
// and therefore not printed. Recompilation regenerates an identical
// PrintMembers, so semantics are preserved.
#if !EXPECTED_OUTPUT
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append("A = ");
builder.Append(A);
return true;
}
#endif
public sealed override string ToString()
{
return "custom";
}
}
public record MimicsGeneratedToString(int A)
{
public sealed override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("MimicsGeneratedToString");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
}
public sealed record SealedRecordWithSealedToString(int A)
{
public sealed override string ToString()
{
return "sealed";
}
}
public record GenericWithSealedToString<T>(T Value)
{
public sealed override string ToString()
{
return typeof(T).Name;
}
}
public record struct StructWithToString(int A)
{
public override string ToString()
{
return "structcustom";
}
}
}
}