mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
241 lines
9.4 KiB
241 lines
9.4 KiB
// 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; |
|
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 |
|
{ |
|
/// <summary> |
|
/// Reads the sequence points out of a portable PDB and compares two PDBs (typically the one |
|
/// the original C# compiler produced versus the one <c>PortablePdbWriter</c> reconstructs) |
|
/// at the granularity that actually matters to a debugging user: the breakpoint map. |
|
/// </summary> |
|
/// <remarks> |
|
/// 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 <em>where the visible (non-hidden) breakpoints land in the source</em>. |
|
/// 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. |
|
/// </remarks> |
|
static class PdbSequencePoints |
|
{ |
|
internal readonly record struct RawSequencePoint(int Offset, bool IsHidden, |
|
int StartLine, int StartColumn, int EndLine, int EndColumn) |
|
{ |
|
/// <summary>Visible source location, formatted with or without columns.</summary> |
|
public string Format(bool includeColumns) |
|
{ |
|
return includeColumns |
|
? $"({StartLine},{StartColumn})-({EndLine},{EndColumn})" |
|
: $"L{StartLine}-{EndLine}"; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public static Dictionary<int, List<RawSequencePoint>> Read(Stream portablePdb) |
|
{ |
|
portablePdb.Position = 0; |
|
using var provider = MetadataReaderProvider.FromPortablePdbStream(portablePdb, MetadataStreamOptions.LeaveOpen); |
|
var reader = provider.GetMetadataReader(); |
|
var result = new Dictionary<int, List<RawSequencePoint>>(); |
|
foreach (var handle in reader.MethodDebugInformation) |
|
{ |
|
var info = reader.GetMethodDebugInformation(handle); |
|
if (info.SequencePointsBlob.IsNil) |
|
continue; |
|
var list = new List<RawSequencePoint>(); |
|
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; |
|
} |
|
|
|
/// <summary> |
|
/// Resolves a readable "Namespace.Type.Method" label for each method-definition row, |
|
/// for use in comparison failure messages. |
|
/// </summary> |
|
public static Dictionary<int, string> ReadMethodNames(string peFileName) |
|
{ |
|
using var peStream = File.OpenRead(peFileName); |
|
using var peReader = new PEReader(peStream); |
|
var md = peReader.GetMetadataReader(); |
|
var names = new Dictionary<int, string>(); |
|
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; |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
/// <param name="expected">PDB used as the oracle (the original compiler's PDB).</param> |
|
/// <param name="actual">PDB under test (the decompiler's reconstruction).</param> |
|
/// <param name="methodNames">Row-to-label map from <see cref="ReadMethodNames"/>.</param> |
|
/// <param name="includeColumns"> |
|
/// When true, breakpoints must match on line <em>and</em> column; when false, only the |
|
/// line span is compared (use for methods where the decompiler's column placement within a |
|
/// statement legitimately differs). |
|
/// </param> |
|
/// <param name="includeHidden"> |
|
/// When true, hidden sequence points are compared too. A hidden point has no source |
|
/// location, so it is described by the visible point it follows ("hidden after <loc>", |
|
/// or "hidden at method start"). Anchoring to a source location keeps the comparison |
|
/// independent of the exact IL offsets, which the decompiler reconstructs differently. |
|
/// </param> |
|
public static string CompareBreakpointMaps( |
|
Dictionary<int, List<RawSequencePoint>> expected, |
|
Dictionary<int, List<RawSequencePoint>> actual, |
|
Dictionary<int, string> methodNames, |
|
bool includeColumns, |
|
bool includeHidden) |
|
{ |
|
var report = new StringBuilder(); |
|
foreach (int row in expected.Keys.Union(actual.Keys).OrderBy(r => r)) |
|
{ |
|
var expectedPoints = Tokens(expected, row, includeColumns, includeHidden); |
|
var actualPoints = Tokens(actual, row, includeColumns, includeHidden); |
|
|
|
if (expectedPoints.SequenceEqual(actualPoints)) |
|
continue; |
|
|
|
methodNames.TryGetValue(row, out var name); |
|
report.Append($"0x{0x06000000 | row:x8} {name}").Append('\n'); |
|
foreach (var (prefix, point) in OrderedDifference(expectedPoints, actualPoints)) |
|
report.Append(prefix).Append(point).Append('\n'); |
|
} |
|
return report.ToString(); |
|
} |
|
|
|
/// <summary> |
|
/// Projects a method's sequence points onto comparable descriptors: each visible point |
|
/// becomes its source location; each hidden point (when included) is anchored to the visible |
|
/// point it follows, so the descriptor does not depend on the reconstructed IL offset. |
|
/// </summary> |
|
static List<string> Tokens(Dictionary<int, List<RawSequencePoint>> map, int row, bool includeColumns, bool includeHidden) |
|
{ |
|
var result = new List<string>(); |
|
if (!map.TryGetValue(row, out var list)) |
|
return result; |
|
string lastVisible = null; |
|
foreach (var sp in list) |
|
{ |
|
if (sp.IsHidden) |
|
{ |
|
if (includeHidden) |
|
result.Add(lastVisible == null ? "hidden at method start" : "hidden after " + lastVisible); |
|
} |
|
else |
|
{ |
|
lastVisible = sp.Format(includeColumns); |
|
result.Add(lastVisible); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
static List<(string Prefix, string Point)> OrderedDifference(List<string> expected, List<string> actual) |
|
{ |
|
int[,] lcs = new int[expected.Count + 1, actual.Count + 1]; |
|
for (int i = expected.Count - 1; i >= 0; i--) |
|
{ |
|
for (int j = actual.Count - 1; j >= 0; j--) |
|
{ |
|
lcs[i, j] = expected[i] == actual[j] |
|
? lcs[i + 1, j + 1] + 1 |
|
: Math.Max(lcs[i + 1, j], lcs[i, j + 1]); |
|
} |
|
} |
|
|
|
var result = new List<(string Prefix, string Point)>(); |
|
for (int i = 0, j = 0; i < expected.Count || j < actual.Count;) |
|
{ |
|
if (i < expected.Count && j < actual.Count && expected[i] == actual[j]) |
|
{ |
|
i++; |
|
j++; |
|
} |
|
else if (j < actual.Count && (i == expected.Count || lcs[i, j + 1] >= lcs[i + 1, j])) |
|
{ |
|
result.Add((" + decompiler only: ", actual[j])); |
|
j++; |
|
} |
|
else |
|
{ |
|
result.Add((" - compiler only: ", expected[i])); |
|
i++; |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// 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 <c>PortablePdbWriter</c> in debug builds). Returns the offending methods, or |
|
/// an empty string when well-formed. |
|
/// </summary> |
|
public static string CheckWellFormed( |
|
Dictionary<int, List<RawSequencePoint>> pdb, |
|
Dictionary<int, string> 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(); |
|
} |
|
} |
|
}
|
|
|