Browse Source

Fix #2879: show stored member-list values for empty ranges

The TypeDef table view derived FieldList/MethodList from the computed
member ranges (TypeDefinition.GetFields/GetMethods), and the MethodDef
view derived ParamList the same way; their FirstOrDefault is a nil
handle for an empty range, so memberless types and parameterless
methods displayed row 0. The stored column value is never 0: it is the
running list position, i.e. the next row's first member or one past the
member table's end. Read the raw columns instead, relative to the row
end so the widths of the preceding string-heap, blob and coded-index
columns need not be re-derived, and sized by the table the column
actually indexes (the FieldPtr/MethodPtr/ParamPtr indirection when
present). EventMap and PropertyMap already read their list columns from
the raw rows. Tooltips explain the empty-list-start semantics; rows
with members behave as before.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3928/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
bf3beea96a
  1. 123
      ILSpy.Tests/Metadata/TypeDefMemberListColumnTests.cs
  2. 29
      ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
  3. 43
      ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs

123
ILSpy.Tests/Metadata/TypeDefMemberListColumnTests.cs

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
// 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.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpy.Metadata;
using ICSharpCode.ILSpy.Metadata.CorTables;
using ICSharpCode.ILSpy.ViewModels;
using Avalonia.Headless.NUnit;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Metadata;
/// <summary>
/// A type that owns no rows in the Field or MethodDef tables: its stored FieldList and
/// MethodList column values are empty-list start positions, not references to own
/// members. An interface cannot acquire an implicit constructor, so memberlessness is
/// structurally guaranteed.
/// </summary>
public interface IMemberlessSampleType
{
}
[TestFixture]
public class TypeDefMemberListColumnTests
{
static PEFile? testAssembly;
static List<TypeDefTableTreeNode.TypeDefEntry>? entries;
static PEFile LoadAssembly()
{
return testAssembly ??= new PEFile(typeof(TypeDefMemberListColumnTests).Assembly.Location);
}
static List<TypeDefTableTreeNode.TypeDefEntry> LoadEntries()
{
if (entries != null)
return entries;
var page = (MetadataTablePageModel)new TypeDefTableTreeNode(LoadAssembly()).CreateTab();
return entries = page.Items.Cast<TypeDefTableTreeNode.TypeDefEntry>().ToList();
}
[OneTimeTearDown]
public void Unload()
{
testAssembly?.Dispose();
}
static int Row(int token) => token & 0x00ffffff;
[AvaloniaTest]
public void MemberlessTypeShowsStoredListValues()
{
var entries = LoadEntries();
var entry = entries.Single(e => e.Name == nameof(IMemberlessSampleType));
// The stored FieldList/MethodList of a memberless type is the running list
// position (the next type's first member row, or one past the table end) -
// never row 0.
Assert.That(Row(entry.FieldList), Is.Not.Zero);
Assert.That(Row(entry.MethodList), Is.Not.Zero);
}
[AvaloniaTest]
public void ParameterlessMethodShowsStoredParamListValue()
{
var page = (MetadataTablePageModel)new MethodTableTreeNode(LoadAssembly()).CreateTab();
var methods = page.Items.Cast<MethodTableTreeNode.MethodDefEntry>().ToList();
int paramRows = testAssembly!.Metadata.GetTableRowCount(TableIndex.Param);
var parameterless = methods.Single(m => m.Name == nameof(ParameterlessMethodShowsStoredParamListValue));
Assert.That(Row(parameterless.ParamList), Is.Not.Zero);
for (int i = 0; i < methods.Count; i++)
{
Assert.That(Row(methods[i].ParamList), Is.InRange(1, paramRows + 1), $"ParamList of row {i + 1}");
if (i > 0)
{
Assert.That(Row(methods[i].ParamList), Is.GreaterThanOrEqualTo(Row(methods[i - 1].ParamList)), $"ParamList of row {i + 1}");
}
}
}
[AvaloniaTest]
public void ListColumnsAreMonotonicallyNonDecreasing()
{
var entries = LoadEntries();
int fieldRows = testAssembly!.Metadata.GetTableRowCount(TableIndex.Field);
int methodRows = testAssembly.Metadata.GetTableRowCount(TableIndex.MethodDef);
for (int i = 0; i < entries.Count; i++)
{
Assert.That(Row(entries[i].FieldList), Is.InRange(1, fieldRows + 1), $"FieldList of row {i + 1}");
Assert.That(Row(entries[i].MethodList), Is.InRange(1, methodRows + 1), $"MethodList of row {i + 1}");
if (i > 0)
{
Assert.That(Row(entries[i].FieldList), Is.GreaterThanOrEqualTo(Row(entries[i - 1].FieldList)), $"FieldList of row {i + 1}");
Assert.That(Row(entries[i].MethodList), Is.GreaterThanOrEqualTo(Row(entries[i - 1].MethodList)), $"MethodList of row {i + 1}");
}
}
}
}

29
ILSpy/Metadata/CorTables/MethodTableTreeNode.cs

@ -41,8 +41,24 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables @@ -41,8 +41,24 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables
protected override IReadOnlyList<MethodDefEntry> LoadTable()
{
var list = new List<MethodDefEntry>();
foreach (var row in metadataFile.Metadata.MethodDefinitions)
list.Add(new MethodDefEntry(metadataFile, row));
var metadata = metadataFile.Metadata;
// ParamList is read from the raw row: the computed range (GetParameters) is empty
// for a parameterless method, but the stored value is the running list position
// (the next method's first Param row, or one past the Param table's end), never 0.
// It is the last column, so it sits at the end of the row; with a ParamPtr
// indirection present, the column indexes the pointer table, whose row count also
// governs the column width.
var indexedTable = metadata.GetTableRowCount(TableIndex.ParamPtr) > 0 ? TableIndex.ParamPtr : TableIndex.Param;
int paramListWidth = metadata.GetTableRowCount(indexedTable) <= ushort.MaxValue ? 2 : 4;
int rowSize = metadata.GetTableRowSize(TableIndex.MethodDef);
int tableOffset = metadata.GetTableMetadataOffset(TableIndex.MethodDef);
var reader = metadata.AsBlobReader();
foreach (var row in metadata.MethodDefinitions)
{
reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(row) - paramListWidth;
int paramList = paramListWidth == 2 ? reader.ReadUInt16() : reader.ReadInt32();
list.Add(new MethodDefEntry(metadataFile, row, paramList));
}
return list;
}
@ -94,20 +110,23 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables @@ -94,20 +110,23 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables
public string? SignatureTooltip => GenerateTooltip(ref signatureTooltip, metadataFile, handle);
[ColumnInfo("X8", Kind = ColumnKind.Token)]
public int ParamList => MetadataTokens.GetToken(methodDef.GetParameters().FirstOrDefault());
public int ParamList => 0x08000000 | paramList;
string? paramListTooltip;
public string? ParamListTooltip {
get {
var param = methodDef.GetParameters().FirstOrDefault();
if (param.IsNil)
return null;
return "(method has no Param rows; the stored value is the start of its empty parameter list: the next method's first Param row, or one past the end of the Param table)";
return GenerateTooltip(ref paramListTooltip, metadataFile, param);
}
}
public MethodDefEntry(MetadataFile metadataFile, MethodDefinitionHandle handle)
readonly int paramList;
public MethodDefEntry(MetadataFile metadataFile, MethodDefinitionHandle handle, int paramList)
{
this.paramList = paramList;
this.metadataFile = metadataFile;
this.handle = handle;
methodDef = metadataFile.Metadata.GetMethodDefinition(handle);

43
ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs

@ -44,9 +44,33 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables @@ -44,9 +44,33 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables
protected override IReadOnlyList<TypeDefEntry> LoadTable()
{
var list = new List<TypeDefEntry>();
foreach (var row in metadataFile.Metadata.TypeDefinitions)
list.Add(new TypeDefEntry(metadataFile, row));
var metadata = metadataFile.Metadata;
// FieldList/MethodList are read from the raw rows: the computed member ranges
// (TypeDefinition.GetFields/GetMethods) are empty for a memberless type, but the
// stored column value is the running list position (the next type's first member
// row, or one past the member table's end), never 0. Reading relative to the row
// end avoids re-deriving the widths of the preceding string-heap and coded-index
// columns. With a FieldPtr/MethodPtr indirection present, the list columns index
// the pointer table, whose row count also governs the column width.
int fieldListWidth = ListColumnWidth(metadata, TableIndex.FieldPtr, TableIndex.Field);
int methodListWidth = ListColumnWidth(metadata, TableIndex.MethodPtr, TableIndex.MethodDef);
int rowSize = metadata.GetTableRowSize(TableIndex.TypeDef);
int tableOffset = metadata.GetTableMetadataOffset(TableIndex.TypeDef);
var reader = metadata.AsBlobReader();
foreach (var row in metadata.TypeDefinitions)
{
reader.Offset = tableOffset + rowSize * MetadataTokens.GetRowNumber(row) - fieldListWidth - methodListWidth;
int fieldList = fieldListWidth == 2 ? reader.ReadUInt16() : reader.ReadInt32();
int methodList = methodListWidth == 2 ? reader.ReadUInt16() : reader.ReadInt32();
list.Add(new TypeDefEntry(metadataFile, row, fieldList, methodList));
}
return list;
static int ListColumnWidth(MetadataReader metadata, TableIndex ptrTable, TableIndex memberTable)
{
var indexed = metadata.GetTableRowCount(ptrTable) > 0 ? ptrTable : memberTable;
return metadata.GetTableRowCount(indexed) <= ushort.MaxValue ? 2 : 4;
}
}
public sealed class TypeDefEntry
@ -103,35 +127,40 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables @@ -103,35 +127,40 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables
}
[ColumnInfo("X8", Kind = ColumnKind.Token)]
public int FieldList => MetadataTokens.GetToken(typeDef.GetFields().FirstOrDefault());
public int FieldList => 0x04000000 | fieldList;
string? fieldListTooltip;
public string? FieldListTooltip {
get {
var @field = typeDef.GetFields().FirstOrDefault();
if (@field.IsNil)
return null;
return "(type has no fields; the stored value is the start of its empty field list: the next type's first field row, or one past the end of the Field table)";
return GenerateTooltip(ref fieldListTooltip, metadataFile, @field);
}
}
[ColumnInfo("X8", Kind = ColumnKind.Token)]
public int MethodList => MetadataTokens.GetToken(typeDef.GetMethods().FirstOrDefault());
public int MethodList => 0x06000000 | methodList;
string? methodListTooltip;
public string? MethodListTooltip {
get {
var method = typeDef.GetMethods().FirstOrDefault();
if (method.IsNil)
return null;
return "(type has no methods; the stored value is the start of its empty method list: the next type's first method row, or one past the end of the MethodDef table)";
return GenerateTooltip(ref methodListTooltip, metadataFile, method);
}
}
public TypeDefEntry(MetadataFile metadataFile, TypeDefinitionHandle handle)
readonly int fieldList;
readonly int methodList;
public TypeDefEntry(MetadataFile metadataFile, TypeDefinitionHandle handle, int fieldList, int methodList)
{
this.metadataFile = metadataFile;
this.handle = handle;
this.fieldList = fieldList;
this.methodList = methodList;
typeDef = metadataFile.Metadata.GetTypeDefinition(handle);
}
}

Loading…
Cancel
Save