diff --git a/ILSpy.Tests/Metadata/TypeDefMemberListColumnTests.cs b/ILSpy.Tests/Metadata/TypeDefMemberListColumnTests.cs
new file mode 100644
index 000000000..3cf2fe019
--- /dev/null
+++ b/ILSpy.Tests/Metadata/TypeDefMemberListColumnTests.cs
@@ -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;
+
+///
+/// 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.
+///
+public interface IMemberlessSampleType
+{
+}
+
+[TestFixture]
+public class TypeDefMemberListColumnTests
+{
+ static PEFile? testAssembly;
+ static List? entries;
+
+ static PEFile LoadAssembly()
+ {
+ return testAssembly ??= new PEFile(typeof(TypeDefMemberListColumnTests).Assembly.Location);
+ }
+
+ static List LoadEntries()
+ {
+ if (entries != null)
+ return entries;
+ var page = (MetadataTablePageModel)new TypeDefTableTreeNode(LoadAssembly()).CreateTab();
+ return entries = page.Items.Cast().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().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}");
+ }
+ }
+ }
+}
diff --git a/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs b/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
index 3ce4c9904..08e7cc035 100644
--- a/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
@@ -41,8 +41,24 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables
protected override IReadOnlyList LoadTable()
{
var list = new List();
- 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
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);
diff --git a/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs b/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs
index 6695c6584..d58bcc0a8 100644
--- a/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs
@@ -44,9 +44,33 @@ namespace ICSharpCode.ILSpy.Metadata.CorTables
protected override IReadOnlyList LoadTable()
{
var list = new List();
- 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
}
[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);
}
}