mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
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:
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user