Browse Source

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
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
ac4f86540d
  1. 105
      ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs
  2. 14
      ILSpy/Metadata/MetadataTablesTreeNode.cs
  3. 12
      ILSpy/SessionSettings.cs

105
ILSpy.Tests/Metadata/HideEmptyMetadataTablesTests.cs

@ -0,0 +1,105 @@ @@ -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<SettingsService>().SessionSettings;
settings.HideEmptyMetadataTables = true;
var window = AppComposition.Current.GetExport<MainWindow>();
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<MetadataTableTreeNode>().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<SettingsService>().SessionSettings;
settings.HideEmptyMetadataTables = false;
try
{
var window = AppComposition.Current.GetExport<MainWindow>();
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<MetadataTableTreeNode>()
.Select(t => t.Kind).ToHashSet();
visibleKinds.Should().Contain(System.Enum.GetValues<TableIndex>(),
"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<AssemblyTreeNode>(assemblyName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
metadataNode.EnsureLazyChildren();
var tables = metadataNode.Children.OfType<MetadataTablesTreeNode>().Single();
tables.EnsureLazyChildren();
metadata = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!.Metadata;
return tables;
}
}

14
ILSpy/Metadata/MetadataTablesTreeNode.cs

@ -23,6 +23,7 @@ using System.Reflection.Metadata.Ecma335; @@ -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 @@ -64,13 +65,24 @@ namespace ILSpy.Metadata
protected override void LoadChildren()
{
var metadata = metadataFile.Metadata;
bool hideEmpty = TryGetHideEmptyMetadataTables();
foreach (var table in Enum.GetValues<TableIndex>())
{
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<SettingsService>().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)

12
ILSpy/SessionSettings.cs

@ -43,6 +43,16 @@ namespace ILSpy @@ -43,6 +43,16 @@ namespace ILSpy
public LanguageSettings LanguageSettings { get; private set; } = null!;
/// <summary>
/// When <see langword="true"/> (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 <see langword="false"/>
/// surfaces every CLI table including empty ones, useful when verifying that a
/// table really has no rows rather than just being filtered out.
/// </summary>
[ObservableProperty]
private bool hideEmptyMetadataTables = true;
[ObservableProperty]
private string? activeAssemblyList;
@ -73,6 +83,7 @@ namespace ILSpy @@ -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 @@ -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))

Loading…
Cancel
Save