Browse Source

Route 43 typed metadata tables onto the datagrid view

The generic MetadataTableTreeNode<TEntry> base now overrides
CreateTab() to return a MetadataTablePageModel populated from
LoadTable() + reflected columns. Every typed CorTable / DebugTable
inherits the grid behaviour for free; the per-table text dump is gone.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
eb8316db29
  1. 58
      ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs
  2. 29
      ILSpy/Metadata/MetadataTableTreeNode.cs

58
ILSpy.Tests/Metadata/TypedMetadataTableTreeTests.cs

@ -39,17 +39,16 @@ namespace ICSharpCode.ILSpy.Tests.Metadata;
public class TypedMetadataTableTreeTests public class TypedMetadataTableTreeTests
{ {
[AvaloniaTest] [AvaloniaTest]
public async Task AssemblyRefTableTreeNode_Decompiles_To_A_Row_Per_AssemblyReference() public async Task AssemblyRefTableTreeNode_Opens_A_Grid_With_One_Row_Per_AssemblyReference()
{ {
// CoreLib's AssemblyRef table holds the assemblies it depends on (typically a // CoreLib's AssemblyRef table is empty (CoreLib is the bottom of the dep chain), so
// handful: System.Runtime, System.Threading, …). Selecting the typed table node // target System.Linq instead. Selecting the typed table node opens a metadata-grid
// should render one row per reference with the reference's display name visible. // tab whose Items list mirrors the table's rows; a known reference's display name
// must show up on at least one row.
var window = AppComposition.Current.GetExport<MainWindow>(); var window = AppComposition.Current.GetExport<MainWindow>();
window.Show(); window.Show();
var vm = (MainWindowViewModel)window.DataContext!; 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); await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq"); var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq");
@ -64,17 +63,18 @@ public class TypedMetadataTableTreeTests
assemblyRefNode.RowCount.Should().BeGreaterThan(0); assemblyRefNode.RowCount.Should().BeGreaterThan(0);
vm.AssemblyTreeModel.SelectNode(assemblyRefNode); vm.AssemblyTreeModel.SelectNode(assemblyRefNode);
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
tab.Text.Should().Contain("AssemblyRef"); tab.Title.Should().StartWith("AssemblyRef");
tab.Text.Should().Contain("Name"); tab.Items.Should().HaveCount(assemblyRefNode.RowCount);
tab.Text.Should().Contain("Version"); var names = tab.Items.Cast<AssemblyRefTableTreeNode.AssemblyRefEntry>()
// CoreLib references almost always include System.Runtime or netstandard. .Select(e => e.Name)
tab.Text.Should().MatchRegex(@"\b(System\.Runtime|netstandard|System\.Private\.CoreLib|System\.Threading)\b"); .ToList();
names.Should().IntersectWith(new[] { "System.Runtime", "netstandard", "System.Private.CoreLib", "System.Threading" });
} }
[AvaloniaTest] [AvaloniaTest]
public async Task TypeDefTableTreeNode_Decompiles_To_A_Row_Per_Type_Definition() public async Task TypeDefTableTreeNode_Opens_A_Grid_Including_System_Object()
{ {
var window = AppComposition.Current.GetExport<MainWindow>(); var window = AppComposition.Current.GetExport<MainWindow>();
window.Show(); window.Show();
@ -91,23 +91,21 @@ public class TypedMetadataTableTreeTests
var typeDefNode = tablesNode.Children.OfType<TypeDefTableTreeNode>().Single(); var typeDefNode = tablesNode.Children.OfType<TypeDefTableTreeNode>().Single();
typeDefNode.Kind.Should().Be(TableIndex.TypeDef); typeDefNode.Kind.Should().Be(TableIndex.TypeDef);
// CoreLib has thousands of type definitions.
typeDefNode.RowCount.Should().BeGreaterThan(1000); typeDefNode.RowCount.Should().BeGreaterThan(1000);
vm.AssemblyTreeModel.SelectNode(typeDefNode); vm.AssemblyTreeModel.SelectNode(typeDefNode);
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
tab.Text.Should().Contain("TypeDef"); tab.Title.Should().StartWith("TypeDef");
// Object is the very first entry every CLI assembly produces (after <Module>). tab.Items.Cast<TypeDefTableTreeNode.TypeDefEntry>()
tab.Text.Should().Contain("Object"); .Should().Contain(e => e.Name == "Object");
} }
[AvaloniaTest] [AvaloniaTest]
public async Task FieldAndMethodTables_Decompile_With_Their_Own_Typed_Leaves() public async Task FieldAndMethodTables_Are_Available_As_Typed_Leaves()
{ {
// Field / MethodDef are by far the largest tables in any real assembly. The typed // Field / MethodDef are by far the largest tables in any real assembly. The typed
// leaf should report the right table kind, expose a row count, and decompile to a // leaves should report the right kind and expose row data through the grid view.
// table whose first column header is "RID" (the per-table row identifier).
var window = AppComposition.Current.GetExport<MainWindow>(); var window = AppComposition.Current.GetExport<MainWindow>();
window.Show(); window.Show();
@ -131,13 +129,13 @@ public class TypedMetadataTableTreeTests
var methodNode = tablesNode.Children.OfType<MethodTableTreeNode>().Single(); var methodNode = tablesNode.Children.OfType<MethodTableTreeNode>().Single();
vm.AssemblyTreeModel.SelectNode(methodNode); vm.AssemblyTreeModel.SelectNode(methodNode);
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
tab.Text.Should().Contain("MethodDef"); tab.Title.Should().StartWith("MethodDef");
tab.Text.Should().Contain("RID"); tab.Columns.Select(c => c.Header.ToString()).Should().Contain("RID");
} }
[AvaloniaTest] [AvaloniaTest]
public async Task ModuleTableTreeNode_Decompiles_To_A_Single_Row_Carrying_The_Module_Name() public async Task ModuleTableTreeNode_Opens_A_Grid_With_The_Single_Module_Row()
{ {
var window = AppComposition.Current.GetExport<MainWindow>(); var window = AppComposition.Current.GetExport<MainWindow>();
window.Show(); window.Show();
@ -156,9 +154,11 @@ public class TypedMetadataTableTreeTests
moduleNode.RowCount.Should().Be(1, "Module is a one-row table by spec"); moduleNode.RowCount.Should().Be(1, "Module is a one-row table by spec");
vm.AssemblyTreeModel.SelectNode(moduleNode); vm.AssemblyTreeModel.SelectNode(moduleNode);
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
tab.Text.Should().Contain("Module"); tab.Title.Should().StartWith("Module");
tab.Text.Should().Contain(".dll"); tab.Items.Should().HaveCount(1);
var row = (ModuleTableTreeNode.ModuleEntry)tab.Items[0];
row.Name.Should().EndWith(".dll");
} }
} }

29
ILSpy/Metadata/MetadataTableTreeNode.cs

@ -21,11 +21,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection.Metadata.Ecma335; using System.Reflection.Metadata.Ecma335;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Metadata;
using ILSpy.Languages;
using ILSpy.TreeNodes; using ILSpy.TreeNodes;
using ILSpy.ViewModels;
namespace ILSpy.Metadata namespace ILSpy.Metadata
{ {
@ -53,15 +52,13 @@ namespace ILSpy.Metadata
} }
/// <summary> /// <summary>
/// Typed companion: holds the row materialiser and the shared text-render path. Rows are /// Typed companion: holds the row materialiser and routes selection to the DataGrid
/// loaded lazily and cached so repeated decompiles don't re-walk the metadata. Phase 2 /// view. Rows are loaded lazily and cached so repeated activations don't re-walk the
/// adds a <c>CreateTab</c> override that hands the same <c>LoadTable()</c> result to a /// metadata. Token-bearing columns surface the runtime hex value here; Phase 3b makes
/// DataGrid view; for now <see cref="ITextOutput"/> is the only renderer. /// them clickable hyperlinks via <see cref="MetadataColumnBuilder"/>.
/// </summary> /// </summary>
public abstract class MetadataTableTreeNode<TEntry> : MetadataTableTreeNode public abstract class MetadataTableTreeNode<TEntry> : MetadataTableTreeNode
{ {
public const int PreviewLimit = 200;
IReadOnlyList<TEntry>? cached; IReadOnlyList<TEntry>? cached;
protected MetadataTableTreeNode(TableIndex kind, MetadataFile metadataFile) protected MetadataTableTreeNode(TableIndex kind, MetadataFile metadataFile)
@ -74,16 +71,10 @@ namespace ILSpy.Metadata
protected abstract IReadOnlyList<TEntry> LoadTable(); protected abstract IReadOnlyList<TEntry> LoadTable();
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) public override TabPageModel CreateTab() => new MetadataTablePageModel {
{ Title = $"{Kind} ({RowCount})",
var rows = cached ??= LoadTable(); Items = (cached ??= LoadTable()).Cast<object>().ToList(),
var preview = rows.Count > PreviewLimit Columns = MetadataColumnBuilder.For<TEntry>(),
? (IReadOnlyList<TEntry>)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)");
}
} }
} }

Loading…
Cancel
Save