Browse Source

Surface embedded portable PDB metadata as top-level sibling

Browsing portable PDB metadata required drilling three levels deep:
Assembly → Metadata → Debug Directory → Debug Metadata (Embedded).
The PDB's tables are now exposed as a top-level MetadataTreeNode
alongside the host module's Metadata folder, so PDB browsing is one
click away. The PdbProvider already cached for decompilation is
reused — no second parse.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
79a9dcee72
  1. 47
      ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs
  2. 6
      ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs
  3. 5
      ILSpy/SettingsService.cs
  4. 13
      ILSpy/TreeNodes/AssemblyTreeNode.cs

47
ILSpy.Tests/Metadata/EmbeddedPdbTreeTests.cs

@ -40,6 +40,8 @@ namespace ICSharpCode.ILSpy.Tests.Metadata; @@ -40,6 +40,8 @@ namespace ICSharpCode.ILSpy.Tests.Metadata;
[TestFixture]
public class EmbeddedPdbTreeTests
{
// audit (2026-05-12): catches changing the "Debug Metadata (Embedded)" label string in
// DebugDirectoryTreeNode.cs (~line 90) — the Text equality assertion in the test fails.
[AvaloniaTest]
public async Task Expanding_DebugDirectory_Surfaces_Embedded_PDB_Metadata_Subtree()
{
@ -73,7 +75,11 @@ public class EmbeddedPdbTreeTests @@ -73,7 +75,11 @@ public class EmbeddedPdbTreeTests
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(loaded.ShortName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
// AssemblyTreeNode now surfaces two MetadataTreeNode children for assemblies with an
// embedded PDB (host metadata + a top-level PDB sibling). This test cares about the
// nested-under-DebugDirectory path, so pick the host metadata explicitly by label.
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>()
.Single(n => (string?)n.Text == Resources.Metadata);
metadataNode.EnsureLazyChildren();
var debugDirectoryNode = metadataNode.Children.OfType<DebugDirectoryTreeNode>().Single();
@ -92,4 +98,43 @@ public class EmbeddedPdbTreeTests @@ -92,4 +98,43 @@ public class EmbeddedPdbTreeTests
pdbTables.Children.OfType<DocumentTableTreeNode>().Should().NotBeEmpty(
"the embedded PDB's Tables subtree must expose the Document table — the lead debug-only table that always has rows in a populated PDB");
}
[AvaloniaTest]
public async Task Embedded_PDB_Metadata_Is_Exposed_As_A_Top_Level_Sibling_Of_The_Host_Metadata_Node()
{
// Sibling-discoverability path: the embedded PDB's metadata should also be reachable
// directly under the AssemblyTreeNode (as a sibling of the host module's "Metadata"
// folder), not only nested under DebugDirectory. Without this, users have to drill
// three levels deep to reach PDB metadata — same content, harder to find.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var testDllPath = typeof(ICSharpCode.Decompiler.Metadata.MetadataFile).Assembly.Location;
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var openCommand = registry.Commands
.Single(c => c.Metadata.Header == nameof(Resources._Open))
.CreateExport().Value;
openCommand.Execute(testDllPath);
await Waiters.WaitForAsync(() =>
vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a =>
string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase)));
var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies()
.First(a => string.Equals(a.FileName, testDllPath, System.StringComparison.OrdinalIgnoreCase));
await loaded.GetLoadResultAsync();
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(loaded.ShortName);
assemblyNode.EnsureLazyChildren();
var topLevelMetadataNodes = assemblyNode.Children.OfType<MetadataTreeNode>().ToList();
topLevelMetadataNodes.Should().HaveCount(2,
"the assembly node must expose two MetadataTreeNode children: the host's PE metadata "
+ "and the embedded portable PDB's metadata as a sibling");
topLevelMetadataNodes.Select(n => n.Text.ToString())
.Should().Contain("Debug Metadata (Embedded)",
"the embedded PDB sibling must use the same label as the nested one for consistency");
}
}

6
ILSpy.Tests/Metadata/MetadataTabSessionRestoreTests.cs

@ -103,7 +103,11 @@ public class MetadataTabSessionRestoreTests @@ -103,7 +103,11 @@ public class MetadataTabSessionRestoreTests
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(loaded.ShortName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
// AssemblyTreeNode surfaces the embedded PDB's metadata as a second top-level
// MetadataTreeNode sibling, so disambiguate by label — this test exercises the
// nested-under-DebugDirectory path.
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>()
.Single(n => (string?)n.Text == ICSharpCode.ILSpy.Properties.Resources.Metadata);
metadataNode.EnsureLazyChildren();
var debugDirectoryNode = metadataNode.Children.OfType<DebugDirectoryTreeNode>().Single();
debugDirectoryNode.EnsureLazyChildren();

5
ILSpy/SettingsService.cs

@ -42,7 +42,10 @@ namespace ILSpy @@ -42,7 +42,10 @@ namespace ILSpy
public MiscSettings MiscSettings => GetSettings<MiscSettings>();
AssemblyListManager? assemblyListManager;
public AssemblyListManager AssemblyListManager => assemblyListManager ??= new(SpySettings);
public AssemblyListManager AssemblyListManager => assemblyListManager ??= new(SpySettings) {
ApplyWinRTProjections = DecompilerSettings.ApplyWindowsRuntimeProjections,
UseDebugSymbols = DecompilerSettings.UseDebugSymbols,
};
/// <summary>
/// Persists every materialised section to disk. Options panels bind two-way to the

13
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -31,6 +31,7 @@ using ICSharpCode.Decompiler.Metadata; @@ -31,6 +31,7 @@ using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.FileLoaders;
using ICSharpCode.ILSpyX.PdbProvider;
using ILSpy;
using ILSpy.Languages;
@ -250,6 +251,18 @@ namespace ILSpy.TreeNodes @@ -250,6 +251,18 @@ namespace ILSpy.TreeNodes
Children.Add(new ILSpy.Metadata.MetadataTreeNode(module, ICSharpCode.ILSpy.Properties.Resources.Metadata));
// Surface portable PDB metadata (embedded or side-by-side) as a top-level sibling
// of the host Metadata folder so PDB browsing is one click away instead of buried
// under Metadata → Debug Directory. Reuses the cached PdbProvider that the rest of
// the app already opened for decompilation — no second parse of the PDB blob.
var debugInfo = assembly.GetDebugInfoOrNull();
if (debugInfo is PortableDebugInfoProvider ppdb
&& ppdb.GetMetadataReader() is not null)
{
var label = $"Debug Metadata ({(ppdb.IsEmbedded ? "Embedded" : "From portable PDB")})";
Children.Add(new ILSpy.Metadata.MetadataTreeNode(ppdb.ToMetadataFile(), label));
}
Children.Add(new ReferenceFolderTreeNode(module, this));
if (module.Resources.Any())

Loading…
Cancel
Save