Browse Source

Metadata:// drill-down + MetadataFile resolution fix

Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
2c5ba5764b
  1. 151
      ILSpy.Tests/Metadata/MetadataProtocolHandlerDrillDownTests.cs
  2. 17
      ILSpy/Metadata/MetadataProtocolHandler.cs
  3. 21
      ILSpy/Metadata/MetadataTreeNode.cs

151
ILSpy.Tests/Metadata/MetadataProtocolHandlerDrillDownTests.cs

@ -0,0 +1,151 @@ @@ -0,0 +1,151 @@
// 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;
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;
/// <summary>
/// Drill-down half of the <c>metadata://</c> protocol handler: when the supplied handle
/// belongs to a metadata table, the handler should resolve to that specific
/// <see cref="MetadataTableTreeNode"/> rather than just landing on the per-assembly
/// <see cref="MetadataTreeNode"/>. WPF's <c>FindNodeByHandleKind</c> is the helper that
/// turns a <see cref="HandleKind"/> into the matching table tree node; this test fixture
/// pins both the helper and the protocol-handler wiring.
/// </summary>
[TestFixture]
public class MetadataProtocolHandlerDrillDownTests
{
[AvaloniaTest]
public async Task FindNodeByHandleKind_Returns_The_TypeDef_Table_For_A_TypeDefinition_Handle()
{
// Direct exercise of the helper. CoreLib is guaranteed to contain a TypeDef table —
// the assert simply checks Kind == TableIndex.TypeDef. The HandleKind→TableIndex cast
// is what the helper relies on; if the underlying enums ever diverge we want a loud
// failure here rather than a silent "always returns null".
var metadataNode = await GetMetadataTreeNodeForCoreLib();
var result = metadataNode.FindNodeByHandleKind(HandleKind.TypeDefinition);
((object?)result).Should().NotBeNull();
result!.Kind.Should().Be(TableIndex.TypeDef, "the TypeDef table backs HandleKind.TypeDefinition");
}
[AvaloniaTest]
public async Task FindNodeByHandleKind_Returns_Null_For_A_Heap_Handle_Kind()
{
// Heap handles (UserString / Blob / Guid / String) don't have a backing
// MetadataTableTreeNode — they live as siblings under the MetadataTreeNode itself.
// The helper must return null in that case so the caller can fall back to the
// per-assembly Metadata node instead of drilling into a wrong table.
var metadataNode = await GetMetadataTreeNodeForCoreLib();
var result = metadataNode.FindNodeByHandleKind(HandleKind.UserString);
((object?)result).Should().BeNull();
}
[AvaloniaTest]
public async Task MetadataProtocolHandler_Drills_Into_TypeDef_Table_For_A_TypeDefinition_Handle()
{
// Full integration: hand a TypeDefinitionHandle to the protocol handler and expect
// the resolved node to be the TypeDef MetadataTableTreeNode under the per-assembly
// Metadata folder. Without the drill-down patch the handler would return the parent
// MetadataTreeNode itself, which makes the user scroll-find the right table by hand.
var (handler, module, _) = await SetUpHandler();
var someTypeDef = module.Metadata.TypeDefinitions.First();
var result = handler.Resolve("metadata", module, someTypeDef, out _);
// SharpTreeNode has a custom .Should() extension in this assembly that hides BeOfType,
// so compare exact types directly.
((object?)result).Should().NotBeNull();
result!.GetType().Should().Be(typeof(TypeDefTableTreeNode),
"TypeDefinition handles must drill into the TypeDef table");
}
[AvaloniaTest]
public async Task MetadataProtocolHandler_Falls_Back_To_MetadataTreeNode_For_Heap_Handles()
{
// String / UserString / Blob / Guid handles don't map to a per-table tree node, so
// the handler stays at the Metadata folder level. Users can still expand to the right
// heap from there; the alternative (returning null) would suppress navigation entirely.
var (handler, module, _) = await SetUpHandler();
// Construct a UserStringHandle via MetadataTokens (the public ctor is internal).
var heapHandle = MetadataTokens.UserStringHandle(1);
var result = handler.Resolve("metadata", module, heapHandle, out _);
((object?)result).Should().NotBeNull();
result!.GetType().Should().Be(typeof(MetadataTreeNode),
"heap handles have no table match, so the handler should keep the user at the Metadata folder");
}
// --- helpers ---------------------------------------------------------------------------
static async Task<MetadataTreeNode> GetMetadataTreeNodeForCoreLib()
{
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 assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
// Drill-down needs the children realised; EnsureLazyChildren cascades only one level.
metadataNode.EnsureLazyChildren();
var tablesNode = metadataNode.Children.OfType<MetadataTablesTreeNode>().Single();
tablesNode.EnsureLazyChildren();
return metadataNode;
}
static async Task<(MetadataProtocolHandler handler, ICSharpCode.Decompiler.Metadata.MetadataFile module, MetadataTreeNode metadataNode)> SetUpHandler()
{
var metadataNode = await GetMetadataTreeNodeForCoreLib();
var vm = (MainWindowViewModel)AppComposition.Current.GetExport<MainWindow>().DataContext!;
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!;
// The handler is exported as IProtocolHandler, not as itself — fish out the
// MetadataProtocolHandler concrete impl from the contract list.
var handler = AppComposition.Current
.GetExports<global::ILSpy.Commands.IProtocolHandler>()
.OfType<MetadataProtocolHandler>()
.Single();
return (handler, module, metadataNode);
}
}

17
ILSpy/Metadata/MetadataProtocolHandler.cs

@ -52,18 +52,23 @@ namespace ILSpy.Metadata @@ -52,18 +52,23 @@ namespace ILSpy.Metadata
newTabPage = true;
if (protocol != "metadata")
return null;
var assemblyNode = assemblyTreeModel.FindTreeNode(module) as AssemblyTreeNode;
// AssemblyTreeModel.FindTreeNode only resolves EntityReference/ITypeDefinition/IMember,
// not MetadataFile — walk the assembly-tree root manually here. Same lookup pattern
// FindTypeNode uses internally.
var assemblyNode = (assemblyTreeModel.Root as AssemblyListTreeNode)?.Children
.OfType<AssemblyTreeNode>()
.FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module);
if (assemblyNode == null)
return null;
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().FirstOrDefault();
if (metadataNode == null)
return null;
// WPF additionally drills into the matching table for the handle kind. The
// Avalonia MetadataTreeNode doesn't yet expose a FindNodeByHandleKind helper;
// surfacing the MetadataTreeNode itself is enough to land the user inside the
// per-assembly metadata view. Specific-table drill-down is a follow-up.
return metadataNode;
// Drill into the matching metadata table when the handle is table-backed. Heap
// handles (String / UserString / Blob / Guid) have no per-table node, so we fall
// back to the per-assembly Metadata folder — the user can expand the right heap
// sibling from there.
return (ILSpyTreeNode?)metadataNode.FindNodeByHandleKind(handle.Kind) ?? metadataNode;
}
}
}

21
ILSpy/Metadata/MetadataTreeNode.cs

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
@ -59,6 +60,26 @@ namespace ILSpy.Metadata @@ -59,6 +60,26 @@ namespace ILSpy.Metadata
DumpMetadataInfo(language, output, metadataFile.Metadata);
}
/// <summary>
/// Resolves a <see cref="HandleKind"/> to the per-table tree node under this metadata
/// folder, or <c>null</c> when the handle has no table (e.g. heap handles: String /
/// UserString / Blob / Guid). Used by <c>MetadataProtocolHandler</c> to drill into
/// the right table when navigating a <c>metadata://</c> reference. Cast from
/// <see cref="HandleKind"/> to <see cref="TableIndex"/> relies on the two enums sharing
/// numeric values for the 0..44 metadata-table range.
/// </summary>
public MetadataTableTreeNode? FindNodeByHandleKind(HandleKind kind)
{
EnsureLazyChildren();
var tables = Children.OfType<MetadataTablesTreeNode>().FirstOrDefault();
if (tables is null)
return null;
tables.EnsureLazyChildren();
return tables.Children
.OfType<MetadataTableTreeNode>()
.FirstOrDefault(x => x.Kind == (TableIndex)kind);
}
internal static void DumpMetadataInfo(Language language, ITextOutput output, MetadataReader metadata)
{
language.WriteCommentLine(output, "MetadataKind: " + metadata.MetadataKind);

Loading…
Cancel
Save