diff --git a/ICSharpCode.Decompiler.Tests/CLAUDE.md b/ICSharpCode.Decompiler.Tests/CLAUDE.md index d5655de3e..7b5f72d1c 100644 --- a/ICSharpCode.Decompiler.Tests/CLAUDE.md +++ b/ICSharpCode.Decompiler.Tests/CLAUDE.md @@ -42,7 +42,7 @@ root `CLAUDE.md` section on the submodule). | Ugly | `UglyTestRunner` / `Ugly/*.cs` | compile -> decompile with sugar settings disabled | sibling `.Expected.cs` file | | Disassembler | `DisassemblerPrettyTestRunner` / `Disassembler/Pretty/*.il` | ilasm -> disassemble with our `ReflectionDisassembler` | the `.il` source (or `.expected.il`, e.g. `SortedOutput`) | | VBPretty | `VBPrettyTestRunner` / `VBPretty/*.vb` | vbc -> decompile to C# | sibling `.cs` file | -| PdbGen | `PdbGenerationTestRunner` / `PdbGen/*.xml` | in-proc Roslyn compile, generate portable PDB with `PortablePdbWriter`, dump both PDBs with `PdbToXmlConverter` | `.expected.xml` | +| PdbGen | `PdbGenerationTestRunner` / `PdbGen/*.xml` | in-proc Roslyn compile (real PDB = oracle), decompile, generate portable PDB with `PortablePdbWriter`, parse both PDBs' sequence-point blobs | the compiler's PDB, projected to the visible breakpoint map (see below) | | Roundtrip | `RoundtripAssembly` (inputs from `ILSpy-tests/`) | whole-project decompile -> MSBuild rebuild -> run original NUnit tests against the rebuilt assembly | test-run success | | Unit tests | `TypeSystem/`, `Semantics/`, `Output/`, `Util/`, `DataFlowTest`, `Metadata/`, `ProjectDecompiler/` | plain in-process NUnit | assertions | @@ -69,8 +69,24 @@ helper, which picks the file via `[CallerMemberName]`. - **ILPretty / Disassembler**: add a `.il` file (assembled with the NuGet ilasm) and the expected `.cs` (`ILPretty`) or rely on round-tripping the `.il` itself (`Disassembler`). - **VBPretty**: add `MyTest.vb` and the expected C# decompilation `MyTest.cs`. -- **PdbGen**: add `MyTest.xml` containing the source files inside `` elements; - the expected PDB dump lives in `MyTest.expected.xml`. +- **PdbGen** (the reconstructed PDB's breakpoints match the C# compiler's): add `MyTest.xml` + containing the source inside `` elements, written exactly as ILSpy + pretty-prints a *single type* (one document per type, no assembly-attribute header - the same + discipline as Pretty). The runner compiles it with Roslyn (whose PDB is the oracle), decompiles, + reconstructs a PDB, and compares only the **visible breakpoint map**: per method, the ordered + source locations of the non-hidden sequence points. IL offsets, hidden sequence points, local + scopes and the embedded source are all dropped, because the decompiler reconstructs them + differently and they never match byte-for-byte. The comparer also runs an oracle-free + well-formedness check (strictly increasing IL offsets = no duplicate/overlapping points). + `TestSequencePoints()` asserts the map matches the compiler exactly; for the handful of methods + where the decompiler legitimately diverges (e.g. it breakpoints a method's opening brace where + the compiler keeps it hidden), call `TestSequencePoints(knownResidual: true)`; the residual is + auto-derived and committed as `MyTest.residual.txt`, so improvements and regressions both flip + the test like a pretty diff. On a mismatch the test writes `MyTest.residual.txt.generated` and + fails; accept a deliberate change by re-running with `ILSPY_ACCEPT_PDB_RESIDUAL=1` set (which + overwrites the snapshot in place) or by copying the `.generated` file over it. `Tolerance.Lines` drops column comparison + for statements whose column placement differs. Nothing is hand-maintained except the `.cs` + source and, rarely, the residual snapshot. No `.expected.*` is committed (all regenerated). ## Conditional expectations (#if) and comparison rules diff --git a/ICSharpCode.Decompiler.Tests/Helpers/PdbSequencePoints.cs b/ICSharpCode.Decompiler.Tests/Helpers/PdbSequencePoints.cs new file mode 100644 index 000000000..224281327 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/Helpers/PdbSequencePoints.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; + +namespace ICSharpCode.Decompiler.Tests.Helpers +{ + /// + /// Reads the sequence points out of a portable PDB and compares two PDBs (typically the one + /// the original C# compiler produced versus the one PortablePdbWriter reconstructs) + /// at the granularity that actually matters to a debugging user: the breakpoint map. + /// + /// + /// The decompiler reconstructs IL ranges, hidden sequence points and local scopes from the + /// ILAst, so those never match the original compiler byte-for-byte. What it can and should + /// get right is where the visible (non-hidden) breakpoints land in the source. + /// The comparison therefore projects each PDB onto the ordered list of visible sequence-point + /// source locations per method (keyed by method-definition row, which is shared between the + /// PDB and the PE it describes) and reports the residual difference. + /// + static class PdbSequencePoints + { + internal readonly record struct RawSequencePoint(int Offset, bool IsHidden, + int StartLine, int StartColumn, int EndLine, int EndColumn) + { + /// Visible source location, formatted with or without columns. + public string Format(bool includeColumns) + { + return includeColumns + ? $"({StartLine},{StartColumn})-({EndLine},{EndColumn})" + : $"L{StartLine}-{EndLine}"; + } + } + + /// + /// Reads every method's sequence points (hidden ones included, in document order) from a + /// portable PDB, keyed by the row number of the owning method definition. + /// + public static Dictionary> Read(Stream portablePdb) + { + portablePdb.Position = 0; + using var provider = MetadataReaderProvider.FromPortablePdbStream(portablePdb, MetadataStreamOptions.LeaveOpen); + var reader = provider.GetMetadataReader(); + var result = new Dictionary>(); + foreach (var handle in reader.MethodDebugInformation) + { + var info = reader.GetMethodDebugInformation(handle); + if (info.SequencePointsBlob.IsNil) + continue; + var list = new List(); + foreach (var sp in info.GetSequencePoints()) + { + list.Add(new RawSequencePoint(sp.Offset, sp.IsHidden, + sp.StartLine, sp.StartColumn, sp.EndLine, sp.EndColumn)); + } + if (list.Count > 0) + { + // A MethodDebugInformation row is parallel to its MethodDefinition row. + result[MetadataTokens.GetRowNumber(handle)] = list; + } + } + return result; + } + + /// + /// Resolves a readable "Namespace.Type.Method" label for each method-definition row, + /// for use in comparison failure messages. + /// + public static Dictionary ReadMethodNames(string peFileName) + { + using var peStream = File.OpenRead(peFileName); + using var peReader = new PEReader(peStream); + var md = peReader.GetMetadataReader(); + var names = new Dictionary(); + foreach (var handle in md.MethodDefinitions) + { + var method = md.GetMethodDefinition(handle); + var type = md.GetTypeDefinition(method.GetDeclaringType()); + string typeName = md.GetString(type.Name); + if (!type.Namespace.IsNil && md.GetString(type.Namespace) is { Length: > 0 } ns) + typeName = ns + "." + typeName; + names[MetadataTokens.GetRowNumber(handle)] = typeName + "." + md.GetString(method.Name); + } + return names; + } + + /// + /// Compares the visible breakpoint maps of two PDBs and returns the residual difference as + /// a deterministic, human-readable report. An empty string means the maps are identical. + /// + /// PDB used as the oracle (the original compiler's PDB). + /// PDB under test (the decompiler's reconstruction). + /// Row-to-label map from . + /// + /// When true, breakpoints must match on line and column; when false, only the + /// line span is compared (use for methods where the decompiler's column placement within a + /// statement legitimately differs). + /// + public static string CompareBreakpointMaps( + Dictionary> expected, + Dictionary> actual, + Dictionary methodNames, + bool includeColumns) + { + var report = new StringBuilder(); + foreach (int row in expected.Keys.Union(actual.Keys).OrderBy(r => r)) + { + var expectedPoints = Visible(expected, row, includeColumns); + var actualPoints = Visible(actual, row, includeColumns); + + var expectedOnly = MultisetDifference(expectedPoints, actualPoints); + var actualOnly = MultisetDifference(actualPoints, expectedPoints); + if (expectedOnly.Count == 0 && actualOnly.Count == 0) + continue; + + methodNames.TryGetValue(row, out var name); + report.Append($"0x{0x06000000 | row:x8} {name}").Append('\n'); + foreach (var p in expectedOnly.OrderBy(p => p)) + report.Append(" - compiler only: ").Append(p).Append('\n'); + foreach (var p in actualOnly.OrderBy(p => p)) + report.Append(" + decompiler only: ").Append(p).Append('\n'); + } + return report.ToString(); + } + + static List Visible(Dictionary> map, int row, bool includeColumns) + { + if (!map.TryGetValue(row, out var list)) + return new List(); + return list.Where(sp => !sp.IsHidden).Select(sp => sp.Format(includeColumns)).ToList(); + } + + static List MultisetDifference(List a, List b) + { + var counts = new Dictionary(); + foreach (var x in b) + counts[x] = counts.GetValueOrDefault(x) + 1; + var result = new List(); + foreach (var x in a) + { + if (counts.TryGetValue(x, out int c) && c > 0) + counts[x] = c - 1; + else + result.Add(x); + } + return result; + } + + /// + /// Checks that the decompiler's own PDB is structurally well-formed, independent of any + /// oracle: within each method the sequence points' IL offsets must be strictly increasing, + /// i.e. no duplicate or overlapping sequence points (a duplicate is exactly the failure + /// that aborts PortablePdbWriter in debug builds). Returns the offending methods, or + /// an empty string when well-formed. + /// + public static string CheckWellFormed( + Dictionary> pdb, + Dictionary methodNames) + { + var report = new StringBuilder(); + foreach (int row in pdb.Keys.OrderBy(r => r)) + { + var list = pdb[row]; + for (int i = 1; i < list.Count; i++) + { + if (list[i].Offset <= list[i - 1].Offset) + { + methodNames.TryGetValue(row, out var name); + report.Append($"0x{0x06000000 | row:x8} {name}: non-increasing IL offset 0x{list[i].Offset:x} after 0x{list[i - 1].Offset:x}").Append('\n'); + break; + } + } + } + return report.ToString(); + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs index e2661e557..8719e4cc8 100644 --- a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Runtime.CompilerServices; -using System.Text; using System.Xml.Linq; using ICSharpCode.Decompiler.CSharp; @@ -13,8 +12,6 @@ using ICSharpCode.Decompiler.DebugInfo; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Tests.Helpers; -using Microsoft.DiaSymReader.Tools; - using NUnit.Framework; namespace ICSharpCode.Decompiler.Tests @@ -24,31 +21,41 @@ namespace ICSharpCode.Decompiler.Tests { static readonly string TestCasePath = Tester.TestCasePath + "/PdbGen"; + /// + /// How strictly a reconstructed breakpoint must match the original compiler's. + /// + enum Tolerance + { + /// Visible breakpoints must match on source line and column. + LinesAndColumns, + /// Only the source line span must match; column placement may differ. + Lines + } + [Test] public void HelloWorld() { - TestGeneratePdb(); + TestSequencePoints(); } [Test] - [Ignore("Missing nested local scopes for loops, differences in IL ranges")] public void ForLoopTests() { - TestGeneratePdb(); + TestSequencePoints(); } [Test] - [Ignore("Differences in IL ranges")] public void LambdaCapturing() { - TestGeneratePdb(); + // The decompiler emits one extra visible breakpoint that the C# compiler keeps hidden; + // the recorded residual pins that known, deliberate difference. + TestSequencePoints(knownResidual: true); } [Test] - [Ignore("Duplicate sequence points for local function")] public void Members() { - TestGeneratePdb(); + TestSequencePoints(knownResidual: true); } [Test] @@ -194,41 +201,78 @@ namespace ICSharpCode.Decompiler.Tests } } - private void TestGeneratePdb([CallerMemberName] string testName = null) + /// + /// Compiles the fixture with the C# compiler (producing a real PDB used as the oracle), + /// decompiles it and reconstructs a PDB with , then compares + /// the two PDBs' visible breakpoint maps. The decompiler reconstructs IL ranges, hidden + /// sequence points and local scopes differently from the compiler, so the comparison drops + /// those and asserts only on the source location of each visible (non-hidden) sequence point. + /// + /// + /// Whether visible breakpoints must match on column as well as line. + /// + /// + /// When false, the breakpoint map must match the compiler exactly (after the projection + /// above). When true, the residual difference must match the committed + /// <testName>.residual.txt snapshot - this pins the known imperfections the + /// decompiler cannot yet reproduce, so an improvement or a regression both flip the test and + /// prompt a deliberate snapshot update (the same accept-the-diff workflow as the pretty tests). + /// + private void TestSequencePoints(Tolerance tolerance = Tolerance.LinesAndColumns, bool knownResidual = false, + [CallerMemberName] string testName = null) { - const PdbToXmlOptions options = PdbToXmlOptions.IncludeEmbeddedSources | PdbToXmlOptions.ThrowOnError | PdbToXmlOptions.IncludeTokens | PdbToXmlOptions.ResolveTokens | PdbToXmlOptions.IncludeMethodSpans; - - string xmlFile = Path.Combine(TestCasePath, testName + ".xml"); (string peFileName, string pdbFileName) = CompileTestCase(testName); - var moduleDefinition = new PEFile(peFileName); - var resolver = new UniversalAssemblyResolver(peFileName, false, moduleDefinition.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage); - var decompiler = new CSharpDecompiler(moduleDefinition, resolver, new DecompilerSettings()); - using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, testName + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite)) + var module = new PEFile(peFileName); + var resolver = new UniversalAssemblyResolver(peFileName, false, module.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage); + var decompiler = new CSharpDecompiler(module, resolver, new DecompilerSettings()); + + using var generatedPdb = new MemoryStream(); + new PortablePdbWriter { NoLogo = true } + .WritePdb(module, decompiler, new DecompilerSettings(), generatedPdb); + + var methodNames = PdbSequencePoints.ReadMethodNames(peFileName); + var actual = PdbSequencePoints.Read(generatedPdb); + Dictionary> expected; + using (var compilerPdb = File.OpenRead(pdbFileName)) + expected = PdbSequencePoints.Read(compilerPdb); + + // Oracle-free structural check on the decompiler's own PDB. + string wellFormed = PdbSequencePoints.CheckWellFormed(actual, methodNames); + Assert.That(wellFormed, Is.Empty, "the reconstructed PDB is not well-formed:\n" + wellFormed); + + string residual = PdbSequencePoints.CompareBreakpointMaps( + expected, actual, methodNames, includeColumns: tolerance == Tolerance.LinesAndColumns); + + if (knownResidual) { - pdbStream.SetLength(0); - new PortablePdbWriter { NoLogo = true } - .WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream); - pdbStream.Position = 0; - using (Stream peStream = File.OpenRead(peFileName)) - using (Stream expectedPdbStream = File.OpenRead(pdbFileName)) + string snapshotFile = Path.Combine(TestCasePath, testName + ".residual.txt"); + string expectedResidual = File.Exists(snapshotFile) + ? File.ReadAllText(snapshotFile).Replace("\r\n", "\n") + : ""; + if (residual != expectedResidual) { - using (StreamWriter writer = new StreamWriter(Path.ChangeExtension(pdbFileName, ".xml"), false, Encoding.UTF8)) + // Setting ILSPY_ACCEPT_PDB_RESIDUAL=1 accepts the new residual in place, so a + // deliberate change is committed by re-running the test with the variable set + // rather than by hand-copying files. + if (Environment.GetEnvironmentVariable("ILSPY_ACCEPT_PDB_RESIDUAL") == "1") { - PdbToXmlConverter.ToXml(writer, expectedPdbStream, peStream, options); + File.WriteAllText(snapshotFile, residual); } - peStream.Position = 0; - using (StreamWriter writer = new StreamWriter(Path.ChangeExtension(xmlFile, ".generated.xml"), false, Encoding.UTF8)) + else { - PdbToXmlConverter.ToXml(writer, pdbStream, peStream, options); + File.WriteAllText(snapshotFile + ".generated", residual); + Assert.Fail($"the breakpoint-map residual changed; review the difference and, if intended, " + + $"accept it by re-running this test with ILSPY_ACCEPT_PDB_RESIDUAL=1 set (or copy " + + $"'{testName}.residual.txt.generated' over '{testName}.residual.txt')." + + $"\n\nExpected:\n{expectedResidual}\nActual:\n{residual}"); } } } - string expectedFileName = Path.ChangeExtension(xmlFile, ".expected.xml"); - ProcessXmlFile(expectedFileName); - string generatedFileName = Path.ChangeExtension(xmlFile, ".generated.xml"); - ProcessXmlFile(generatedFileName); - CodeAssert.AreEqual(Normalize(expectedFileName), Normalize(generatedFileName)); + else + { + Assert.That(residual, Is.Empty, "the reconstructed breakpoint map differs from the compiler's:\n" + residual); + } } private (string peFileName, string pdbFileName) CompileTestCase(string testName) @@ -244,41 +288,5 @@ namespace ICSharpCode.Decompiler.Tests return (peFileName, pdbFileName); } - - private void ProcessXmlFile(string fileName) - { - var document = XDocument.Load(fileName); - foreach (var file in document.Descendants("file")) - { - file.Attribute("checksum").Remove(); - file.Attribute("embeddedSourceLength")?.Remove(); - var name = file.Attribute("name"); - if (name != null) - { - // Generated document names use the platform's directory separator; - // the expected files were produced on Windows. - name.Value = name.Value.Replace('/', '\\'); - } - file.ReplaceNodes(new XCData(file.Value.Replace("\uFEFF", ""))); - } - document.Save(fileName, SaveOptions.None); - } - - private string Normalize(string inputFileName) - { - return File.ReadAllText(inputFileName).Replace("\r\n", "\n").Replace("\r", "\n"); - } - } - - class StringWriterWithEncoding : StringWriter - { - readonly Encoding encoding; - - public StringWriterWithEncoding(Encoding encoding) - { - this.encoding = encoding ?? throw new ArgumentNullException("encoding"); - } - - public override Encoding Encoding => encoding; } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/.gitignore b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/.gitignore index ae2ac11dc..96f4c7ba8 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/.gitignore +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/.gitignore @@ -2,3 +2,4 @@ /*.pdb /*.expected.xml /*.generated.xml +/*.residual.txt.generated diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml index 492f58104..39e8fc01e 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml @@ -28,45 +28,4 @@ public class ForLoopTests } ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/HelloWorld.xml b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/HelloWorld.xml index 90f2d542b..e0708da65 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/HelloWorld.xml +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/HelloWorld.xml @@ -16,20 +16,4 @@ public class HelloWorld } ]]> - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.residual.txt b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.residual.txt new file mode 100644 index 000000000..e30fd63ec --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.residual.txt @@ -0,0 +1,2 @@ +0x06000001 ICSharpCode.Decompiler.Tests.TestCases.PdbGen.LambdaCapturing.Main + + decompiler only: (8,2)-(8,3) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.xml b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.xml index ac478edb0..5446ad889 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.xml +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/LambdaCapturing.xml @@ -21,42 +21,4 @@ public class LambdaCapturing } ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.residual.txt b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.residual.txt new file mode 100644 index 000000000..54e218660 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.residual.txt @@ -0,0 +1,6 @@ +0x0600000b C..ctor + - compiler only: (50,2)-(50,12) + + decompiler only: (51,2)-(51,3) +0x0600000c C.Finalize + - compiler only: (56,2)-(56,3) + - compiler only: (57,2)-(57,3) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.xml b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.xml index 8e25eed1c..7c35954e4 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.xml +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/Members.xml @@ -1,95 +1,86 @@ - - 42; - private int Property - { - get + private int Property + { + get { return 0; } - set + set { } - } + } - private C this[int index] => null; + private C this[int index] => null; - private C this[string s] - { - get + private C this[string s] + { + get { return null; } - set + set { } - } + } - public event Action Event - { - add - { - } - remove - { - } - } + public event Action Event + { + add + { + } + remove + { + } + } - public static implicit operator C(int i) - { - return null; - } + public static implicit operator C(int i) + { + return null; + } - static C() - { - } + static C() + { + } - public C() - { + public C() + { Console.WriteLine(); - } - - ~C() - { - } + } - void IDisposable.Dispose() - { - } + ~C() + { + } - private static void Main() - { - C c = new C(); + void IDisposable.Dispose() + { + } - c.Event += delegate + private static void Main() + { + C c = new C(); + c.Event += delegate { }; - _ = c.Property; - _ = c.ExpressionProperty; - _ = c[0]; - _ = c[""]; - - _ = (C)1; - - Local(); - - static void Local() - { + _ = c.Property; + _ = c.ExpressionProperty; + _ = c[0]; + _ = c[""]; + _ = (C)1; + Local(); + static void Local() + { Console.WriteLine(); - } - } + } + } } ]]> - - - - - \ No newline at end of file +