From 688564c94ae6995c9756005ed4c63884b1dcc011 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 7 May 2026 14:21:50 +0200 Subject: [PATCH] Typed table-leaf base + first 4 cor tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the generic MetadataTableTreeNode base — caches LoadTable's result and runs it through MetadataTextWriter for the Phase 1 text dump. Ports Module / TypeRef / TypeDef / AssemblyRef as the first concrete leaves; the remaining 31 CorTables and 8 DebugTables continue to land through the universal placeholder until subsequent commits replace them. Also fixes MetadataTextWriter to convert enum values to their underlying integer before applying digit-bearing format strings, since Enum.ToString rejects "X8". Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../Metadata/TypedMetadataTableTreeTests.cs | 130 ++++++++++++++++++ .../CorTables/AssemblyRefTableTreeNode.cs | 85 ++++++++++++ .../Metadata/CorTables/ModuleTableTreeNode.cs | 79 +++++++++++ .../CorTables/TypeDefTableTreeNode.cs | 90 ++++++++++++ .../CorTables/TypeRefTableTreeNode.cs | 78 +++++++++++ ILSpy/Metadata/MetadataTableTreeNode.cs | 39 ++++++ ILSpy/Metadata/MetadataTablesTreeNode.cs | 12 +- ILSpy/Metadata/MetadataTextWriter.cs | 11 +- 8 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs create mode 100644 ILSpy/Metadata/CorTables/AssemblyRefTableTreeNode.cs create mode 100644 ILSpy/Metadata/CorTables/ModuleTableTreeNode.cs create mode 100644 ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs create mode 100644 ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs diff --git a/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs b/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs new file mode 100644 index 000000000..f3e0804da --- /dev/null +++ b/ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ILSpy.AppEnv; +using ILSpy.Metadata; +using ILSpy.Metadata.CorTables; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Metadata; + +[TestFixture] +public class TypedMetadataTableTreeTests +{ + [AvaloniaTest] + public async Task AssemblyRefTableTreeNode_Decompiles_To_A_Row_Per_AssemblyReference() + { + // CoreLib's AssemblyRef table holds the assemblies it depends on (typically a + // handful: System.Runtime, System.Threading, …). Selecting the typed table node + // should render one row per reference with the reference's display name visible. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + // CoreLib has zero AssemblyRefs (it's the bottom of the dep chain), so target + // System.Linq instead — same approach as the broader assembly-tree tests. + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var tablesNode = metadataNode.Children.OfType().Single(); + tablesNode.EnsureLazyChildren(); + var assemblyRefNode = tablesNode.Children.OfType().Single(); + + assemblyRefNode.Kind.Should().Be(TableIndex.AssemblyRef); + assemblyRefNode.RowCount.Should().BeGreaterThan(0); + + vm.AssemblyTreeModel.SelectNode(assemblyRefNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("AssemblyRef"); + tab.Text.Should().Contain("Name"); + tab.Text.Should().Contain("Version"); + // CoreLib references almost always include System.Runtime or netstandard. + tab.Text.Should().MatchRegex(@"\b(System\.Runtime|netstandard|System\.Private\.CoreLib|System\.Threading)\b"); + } + + [AvaloniaTest] + public async Task TypeDefTableTreeNode_Decompiles_To_A_Row_Per_Type_Definition() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var tablesNode = metadataNode.Children.OfType().Single(); + tablesNode.EnsureLazyChildren(); + var typeDefNode = tablesNode.Children.OfType().Single(); + + typeDefNode.Kind.Should().Be(TableIndex.TypeDef); + // CoreLib has thousands of type definitions. + typeDefNode.RowCount.Should().BeGreaterThan(1000); + + vm.AssemblyTreeModel.SelectNode(typeDefNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("TypeDef"); + // Object is the very first entry every CLI assembly produces (after ). + tab.Text.Should().Contain("Object"); + } + + [AvaloniaTest] + public async Task ModuleTableTreeNode_Decompiles_To_A_Single_Row_Carrying_The_Module_Name() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var tablesNode = metadataNode.Children.OfType().Single(); + tablesNode.EnsureLazyChildren(); + var moduleNode = tablesNode.Children.OfType().Single(); + + moduleNode.RowCount.Should().Be(1, "Module is a one-row table by spec"); + + vm.AssemblyTreeModel.SelectNode(moduleNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("Module"); + tab.Text.Should().Contain(".dll"); + } +} diff --git a/ILSpy/Metadata/CorTables/AssemblyRefTableTreeNode.cs b/ILSpy/Metadata/CorTables/AssemblyRefTableTreeNode.cs new file mode 100644 index 000000000..a562b51ef --- /dev/null +++ b/ILSpy/Metadata/CorTables/AssemblyRefTableTreeNode.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler.Metadata; + +namespace ILSpy.Metadata.CorTables +{ + /// + /// View of the AssemblyRef table — every external assembly the module imports. Each row + /// carries the reference's name, version, culture, and the public-key-or-token blob + /// that pins it to a specific signing identity. + /// + public sealed class AssemblyRefTableTreeNode : MetadataTableTreeNode + { + public AssemblyRefTableTreeNode(MetadataFile metadataFile) + : base(TableIndex.AssemblyRef, metadataFile) + { + } + + protected override IReadOnlyList LoadTable() + { + var list = new List(); + foreach (var row in metadataFile.Metadata.AssemblyReferences) + list.Add(new AssemblyRefEntry(metadataFile, row)); + return list; + } + + public sealed class AssemblyRefEntry + { + readonly MetadataFile metadataFile; + readonly AssemblyReferenceHandle handle; + readonly System.Reflection.Metadata.AssemblyReference assemblyRef; + + public int RID => MetadataTokens.GetRowNumber(handle); + + [ColumnInfo("X8")] + public int Token => MetadataTokens.GetToken(handle); + + [ColumnInfo("X8")] + public int Offset => metadataFile.MetadataOffset + + metadataFile.Metadata.GetTableMetadataOffset(TableIndex.AssemblyRef) + + metadataFile.Metadata.GetTableRowSize(TableIndex.AssemblyRef) * (RID - 1); + + public Version Version => assemblyRef.Version; + + [ColumnInfo("X8")] + public AssemblyFlags Flags => assemblyRef.Flags; + + [ColumnInfo("X8", Kind = ColumnKind.HeapOffset)] + public int PublicKeyOrToken => MetadataTokens.GetHeapOffset(assemblyRef.PublicKeyOrToken); + + public string Name => metadataFile.Metadata.GetString(assemblyRef.Name); + + public string Culture => metadataFile.Metadata.GetString(assemblyRef.Culture); + + public AssemblyRefEntry(MetadataFile metadataFile, AssemblyReferenceHandle handle) + { + this.metadataFile = metadataFile; + this.handle = handle; + assemblyRef = metadataFile.Metadata.GetAssemblyReference(handle); + } + } + } +} diff --git a/ILSpy/Metadata/CorTables/ModuleTableTreeNode.cs b/ILSpy/Metadata/CorTables/ModuleTableTreeNode.cs new file mode 100644 index 000000000..a143bc5f3 --- /dev/null +++ b/ILSpy/Metadata/CorTables/ModuleTableTreeNode.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler.Metadata; + +namespace ILSpy.Metadata.CorTables +{ + /// + /// View of the Module table — always exactly one row carrying the module name and the + /// MVID (the GUID identifying this module's identity). Reads the GUID heap for MVID and + /// the optional generation IDs (only present in EnC / hot-reload deltas). + /// + public sealed class ModuleTableTreeNode : MetadataTableTreeNode + { + public ModuleTableTreeNode(MetadataFile metadataFile) + : base(TableIndex.Module, metadataFile) + { + } + + protected override IReadOnlyList LoadTable() + => [new ModuleEntry(metadataFile, (ModuleDefinitionHandle)EntityHandle.ModuleDefinition)]; + + public sealed class ModuleEntry + { + readonly MetadataFile metadataFile; + readonly ModuleDefinitionHandle handle; + readonly ModuleDefinition moduleDef; + + public int RID => MetadataTokens.GetRowNumber(handle); + + [ColumnInfo("X8")] + public int Token => MetadataTokens.GetToken(handle); + + [ColumnInfo("X8")] + public int Offset => metadataFile.MetadataOffset + + metadataFile.Metadata.GetTableMetadataOffset(TableIndex.Module) + + metadataFile.Metadata.GetTableRowSize(TableIndex.Module) * (RID - 1); + + public int Generation => moduleDef.Generation; + + public string Name => metadataFile.Metadata.GetString(moduleDef.Name); + + [ColumnInfo("X8")] + public int Mvid => MetadataTokens.GetHeapOffset(moduleDef.Mvid); + + [ColumnInfo("X8")] + public int GenerationId => MetadataTokens.GetHeapOffset(moduleDef.GenerationId); + + [ColumnInfo("X8")] + public int BaseGenerationId => MetadataTokens.GetHeapOffset(moduleDef.BaseGenerationId); + + public ModuleEntry(MetadataFile metadataFile, ModuleDefinitionHandle handle) + { + this.metadataFile = metadataFile; + this.handle = handle; + moduleDef = metadataFile.Metadata.GetModuleDefinition(); + } + } + } +} diff --git a/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs b/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs new file mode 100644 index 000000000..5aac48fd2 --- /dev/null +++ b/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler.Metadata; + +namespace ILSpy.Metadata.CorTables +{ + /// + /// View of the TypeDef table — every type the module defines. The first row is the + /// pseudo-type <Module>, owning module-scoped fields and methods. Each row carries + /// attributes (visibility, layout, semantics), the optional base type, and pointers + /// into the FieldList / MethodList for the type's members. + /// + public sealed class TypeDefTableTreeNode : MetadataTableTreeNode + { + public TypeDefTableTreeNode(MetadataFile metadataFile) + : base(TableIndex.TypeDef, metadataFile) + { + } + + protected override IReadOnlyList LoadTable() + { + var list = new List(); + foreach (var row in metadataFile.Metadata.TypeDefinitions) + list.Add(new TypeDefEntry(metadataFile, row)); + return list; + } + + public sealed class TypeDefEntry + { + readonly MetadataFile metadataFile; + readonly TypeDefinitionHandle handle; + readonly TypeDefinition typeDef; + + public int RID => MetadataTokens.GetRowNumber(handle); + + [ColumnInfo("X8")] + public int Token => MetadataTokens.GetToken(handle); + + [ColumnInfo("X8")] + public int Offset => metadataFile.MetadataOffset + + metadataFile.Metadata.GetTableMetadataOffset(TableIndex.TypeDef) + + metadataFile.Metadata.GetTableRowSize(TableIndex.TypeDef) * (RID - 1); + + [ColumnInfo("X8")] + public TypeAttributes Attributes => typeDef.Attributes; + + public string Name => metadataFile.Metadata.GetString(typeDef.Name); + + public string Namespace => metadataFile.Metadata.GetString(typeDef.Namespace); + + [ColumnInfo("X8", Kind = ColumnKind.Token)] + public int BaseType => MetadataTokens.GetToken(typeDef.BaseType); + + [ColumnInfo("X8", Kind = ColumnKind.Token)] + public int FieldList => MetadataTokens.GetToken(typeDef.GetFields().FirstOrDefault()); + + [ColumnInfo("X8", Kind = ColumnKind.Token)] + public int MethodList => MetadataTokens.GetToken(typeDef.GetMethods().FirstOrDefault()); + + public TypeDefEntry(MetadataFile metadataFile, TypeDefinitionHandle handle) + { + this.metadataFile = metadataFile; + this.handle = handle; + typeDef = metadataFile.Metadata.GetTypeDefinition(handle); + } + } + } +} diff --git a/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs b/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs new file mode 100644 index 000000000..7eba9c933 --- /dev/null +++ b/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs @@ -0,0 +1,78 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler.Metadata; + +namespace ILSpy.Metadata.CorTables +{ + /// + /// View of the TypeRef table — every external type the module mentions. Each row points + /// at the resolution scope (an AssemblyRef, ModuleRef, or another TypeRef for nested + /// types) plus the namespace + name of the referenced type. + /// + public sealed class TypeRefTableTreeNode : MetadataTableTreeNode + { + public TypeRefTableTreeNode(MetadataFile metadataFile) + : base(TableIndex.TypeRef, metadataFile) + { + } + + protected override IReadOnlyList LoadTable() + { + var list = new List(); + foreach (var row in metadataFile.Metadata.TypeReferences) + list.Add(new TypeRefEntry(metadataFile, row)); + return list; + } + + public sealed class TypeRefEntry + { + readonly MetadataFile metadataFile; + readonly TypeReferenceHandle handle; + readonly TypeReference typeRef; + + public int RID => MetadataTokens.GetRowNumber(handle); + + [ColumnInfo("X8")] + public int Token => MetadataTokens.GetToken(handle); + + [ColumnInfo("X8")] + public int Offset => metadataFile.MetadataOffset + + metadataFile.Metadata.GetTableMetadataOffset(TableIndex.TypeRef) + + metadataFile.Metadata.GetTableRowSize(TableIndex.TypeRef) * (RID - 1); + + [ColumnInfo("X8", Kind = ColumnKind.Token)] + public int ResolutionScope => MetadataTokens.GetToken(typeRef.ResolutionScope); + + public string Name => metadataFile.Metadata.GetString(typeRef.Name); + + public string Namespace => metadataFile.Metadata.GetString(typeRef.Namespace); + + public TypeRefEntry(MetadataFile metadataFile, TypeReferenceHandle handle) + { + this.metadataFile = metadataFile; + this.handle = handle; + typeRef = metadataFile.Metadata.GetTypeReference(handle); + } + } + } +} diff --git a/ILSpy/Metadata/MetadataTableTreeNode.cs b/ILSpy/Metadata/MetadataTableTreeNode.cs index 0710e5917..93242c562 100644 --- a/ILSpy/Metadata/MetadataTableTreeNode.cs +++ b/ILSpy/Metadata/MetadataTableTreeNode.cs @@ -17,10 +17,14 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection.Metadata.Ecma335; +using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; +using ILSpy.Languages; using ILSpy.TreeNodes; namespace ILSpy.Metadata @@ -47,4 +51,39 @@ namespace ILSpy.Metadata public override object Icon => Images.Images.MetadataTable; } + + /// + /// Typed companion: holds the row materialiser and the shared text-render path. Rows are + /// loaded lazily and cached so repeated decompiles don't re-walk the metadata. Phase 2 + /// adds a CreateTab override that hands the same LoadTable() result to a + /// DataGrid view; for now is the only renderer. + /// + public abstract class MetadataTableTreeNode : MetadataTableTreeNode + { + public const int PreviewLimit = 200; + + IReadOnlyList? cached; + + protected MetadataTableTreeNode(TableIndex kind, MetadataFile metadataFile) + : base(kind, metadataFile) + { + } + + public override object Text => $"{(byte)Kind:X2} {Kind} ({RowCount})"; + public override string ToString() => Kind.ToString(); + + protected abstract IReadOnlyList LoadTable(); + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + var rows = cached ??= LoadTable(); + var preview = rows.Count > PreviewLimit + ? (IReadOnlyList)rows.Take(PreviewLimit).ToList() + : rows; + language.WriteCommentLine(output, $"{Kind} ({rows.Count} rows)"); + MetadataTextWriter.WriteTable(language, output, preview); + if (rows.Count > preview.Count) + language.WriteCommentLine(output, $"... ({rows.Count - preview.Count} more rows; full view ships with the metadata DataGrid in a future release)"); + } + } } diff --git a/ILSpy/Metadata/MetadataTablesTreeNode.cs b/ILSpy/Metadata/MetadataTablesTreeNode.cs index 660ac134e..eaba79aea 100644 --- a/ILSpy/Metadata/MetadataTablesTreeNode.cs +++ b/ILSpy/Metadata/MetadataTablesTreeNode.cs @@ -24,6 +24,7 @@ using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; using ILSpy.Languages; +using ILSpy.Metadata.CorTables; using ILSpy.TreeNodes; namespace ILSpy.Metadata @@ -69,8 +70,15 @@ namespace ILSpy.Metadata } } - // Phase 1e replaces this universal fallback with typed per-table subclasses. + // Typed leaves are added in passes (1e-i, 1e-ii, 1e-iii); any table not yet ported + // falls through to the universal placeholder so the navigation surface stays whole. internal static MetadataTableTreeNode CreateTableTreeNode(TableIndex table, MetadataFile metadataFile) - => new UnsupportedMetadataTableTreeNode(table, metadataFile); + => table switch { + TableIndex.Module => new ModuleTableTreeNode(metadataFile), + TableIndex.TypeRef => new TypeRefTableTreeNode(metadataFile), + TableIndex.TypeDef => new TypeDefTableTreeNode(metadataFile), + TableIndex.AssemblyRef => new AssemblyRefTableTreeNode(metadataFile), + _ => new UnsupportedMetadataTableTreeNode(table, metadataFile), + }; } } diff --git a/ILSpy/Metadata/MetadataTextWriter.cs b/ILSpy/Metadata/MetadataTextWriter.cs index fc5ece896..86a917c5f 100644 --- a/ILSpy/Metadata/MetadataTextWriter.cs +++ b/ILSpy/Metadata/MetadataTextWriter.cs @@ -108,8 +108,15 @@ namespace ILSpy.Metadata return FormatHex(value, entry.Size * 2); } - if (!string.IsNullOrEmpty(format) && value is IFormattable formattable) - return formattable.ToString(format, CultureInfo.InvariantCulture); + if (!string.IsNullOrEmpty(format)) + { + // Enum.ToString rejects digit-bearing format strings ("X8" etc.); coerce to the + // underlying integer first so the column lines up with neighbouring hex cells. + if (value is Enum enumValue) + value = Convert.ChangeType(enumValue, enumValue.GetTypeCode(), CultureInfo.InvariantCulture)!; + if (value is IFormattable formattable) + return formattable.ToString(format, CultureInfo.InvariantCulture); + } return value.ToString() ?? ""; }