From ac4f86540ddff8b843d9fc8e37fc3e6174a77d39 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 14:33:08 +0200 Subject: [PATCH] Thread HideEmptyMetadataTables setting through Tables tree SessionSettings now carries HideEmptyMetadataTables (default true, matching prior behavior) and MetadataTablesTreeNode honors it: when true, tables with zero rows are hidden; when false, every CLI TableIndex is surfaced. The setting persists across launches via ILSpy.xml. There's no options dialog yet to flip it, but the plumbing is in place for a future settings UI to toggle. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../Metadata/HideEmptyMetadataTablesTests.cs | 105 ++++++++++++++++++ ILSpy/Metadata/MetadataTablesTreeNode.cs | 14 ++- ILSpy/SessionSettings.cs | 12 ++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs diff --git a/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs b/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs new file mode 100644 index 000000000..b8e97a5a8 --- /dev/null +++ b/ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs @@ -0,0 +1,105 @@ +// 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; +using ILSpy.AppEnv; +using ILSpy.Metadata; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Metadata; + +[TestFixture] +public class HideEmptyMetadataTablesTests +{ + [AvaloniaTest] + public async Task Default_Setting_Hides_Tables_With_Zero_Rows() + { + // Default SessionSettings.HideEmptyMetadataTables = true: every child of the + // Tables sub-tree must correspond to a non-zero CLI table. + + var settings = AppComposition.Current.GetExport().SessionSettings; + settings.HideEmptyMetadataTables = true; + + 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 tables = LoadTablesNode(vm, coreLibName, out var metadata); + + tables.Children.OfType().Should().AllSatisfy(t => + metadata.GetTableRowCount(t.Kind).Should().BeGreaterThan(0, + "with HideEmptyMetadataTables=true, every visible table must have at least one row")); + } + + [AvaloniaTest] + public async Task Disabling_The_Setting_Surfaces_Every_Table_Even_When_Empty() + { + // Flip HideEmptyMetadataTables off and confirm a table that's known empty for + // CoreLib (FieldRva, which is rare in modern managed code) shows up among the + // children. + + var settings = AppComposition.Current.GetExport().SessionSettings; + settings.HideEmptyMetadataTables = false; + try + { + 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 tables = LoadTablesNode(vm, coreLibName, out var _); + + var visibleKinds = tables.Children.OfType() + .Select(t => t.Kind).ToHashSet(); + visibleKinds.Should().Contain(System.Enum.GetValues(), + "with HideEmptyMetadataTables=false, every CLI TableIndex must surface as a child"); + } + finally + { + settings.HideEmptyMetadataTables = true; + } + } + + static MetadataTablesTreeNode LoadTablesNode(MainWindowViewModel vm, string assemblyName, + out System.Reflection.Metadata.MetadataReader metadata) + { + var assemblyNode = vm.AssemblyTreeModel.FindNode(assemblyName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var tables = metadataNode.Children.OfType().Single(); + tables.EnsureLazyChildren(); + metadata = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!.Metadata; + return tables; + } +} diff --git a/ILSpy/Metadata/MetadataTablesTreeNode.cs b/ILSpy/Metadata/MetadataTablesTreeNode.cs index d371fb6a5..31d7032ff 100644 --- a/ILSpy/Metadata/MetadataTablesTreeNode.cs +++ b/ILSpy/Metadata/MetadataTablesTreeNode.cs @@ -23,6 +23,7 @@ using System.Reflection.Metadata.Ecma335; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; +using ILSpy.AppEnv; using ILSpy.Languages; using ILSpy.Metadata.CorTables; using ILSpy.Metadata.DebugTables; @@ -64,13 +65,24 @@ namespace ILSpy.Metadata protected override void LoadChildren() { var metadata = metadataFile.Metadata; + bool hideEmpty = TryGetHideEmptyMetadataTables(); foreach (var table in Enum.GetValues()) { - if (metadata.GetTableRowCount(table) > 0) + if (!hideEmpty || metadata.GetTableRowCount(table) > 0) Children.Add(CreateTableTreeNode(table, metadataFile)); } } + static bool TryGetHideEmptyMetadataTables() + { + // Composition isn't always available (design-time previews, isolated tests that + // build the tree directly without booting the app); fall back to the same default + // SessionSettings exposes — keep empty tables hidden to match decade-old WPF UX. + try + { return AppComposition.Current.GetExport().SessionSettings.HideEmptyMetadataTables; } + catch { return true; } + } + // 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) diff --git a/ILSpy/SessionSettings.cs b/ILSpy/SessionSettings.cs index 7fcb7bd49..59f658d23 100644 --- a/ILSpy/SessionSettings.cs +++ b/ILSpy/SessionSettings.cs @@ -43,6 +43,16 @@ namespace ILSpy public LanguageSettings LanguageSettings { get; private set; } = null!; + /// + /// When (default), the Metadata Tables sub-tree only lists + /// tables whose row count is non-zero — keeps the tree compact for assemblies that + /// don't use, say, GenericParam or ImplMap. Setting it to + /// surfaces every CLI table including empty ones, useful when verifying that a + /// table really has no rows rather than just being filtered out. + /// + [ObservableProperty] + private bool hideEmptyMetadataTables = true; + [ObservableProperty] private string? activeAssemblyList; @@ -73,6 +83,7 @@ namespace ILSpy LanguageSettings = new LanguageSettings(filterSettings, this); LanguageSettings.PropertyChanged += (s, e) => OnPropertyChanged(nameof(LanguageSettings)); + HideEmptyMetadataTables = (bool?)section.Element(nameof(HideEmptyMetadataTables)) ?? true; ActiveAssemblyList = (string?)section.Element("ActiveAssemblyList"); ActiveLanguageName = (string?)section.Element("ActiveLanguageName"); ActiveTreeViewPath = section.Element("ActiveTreeViewPath")?.Elements().Select(e => (string)e).ToArray(); @@ -98,6 +109,7 @@ namespace ILSpy var section = new XElement(SectionName); if (LanguageSettings != null) section.Add(LanguageSettings.SaveAsXml()); + section.Add(new XElement(nameof(HideEmptyMetadataTables), HideEmptyMetadataTables)); if (!string.IsNullOrEmpty(ActiveAssemblyList)) section.Add(new XElement("ActiveAssemblyList", ActiveAssemblyList)); if (!string.IsNullOrEmpty(ActiveLanguageName))