From 738ae561dd181b59fa1eab94a636aa71ae170e5a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 9 Jun 2026 14:28:00 +0200 Subject: [PATCH] Add EntityReference.Resolve for the shared handle resolution The "resolve assembly -> entity handle -> uncached DecompilerTypeSystem -> ResolveEntity" sequence was copy-pasted in the code-view hover navigation and the assembly-tree node finder. Move it onto EntityReference as a Resolve(AssemblyList) member so both call sites just supply the list they already have, and the uncached-per-call decision lives in one place. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/EntityReferenceResolveTests.cs | 66 ++++++++++++++++++++++ ILSpy/AssemblyTree/AssemblyTreeModel.cs | 9 +-- ILSpy/EntityReference.cs | 21 +++++++ ILSpy/TextView/DecompilerTextView.axaml.cs | 11 +--- 4 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 ILSpy.Tests/EntityReferenceResolveTests.cs diff --git a/ILSpy.Tests/EntityReferenceResolveTests.cs b/ILSpy.Tests/EntityReferenceResolveTests.cs new file mode 100644 index 000000000..b5af5d67b --- /dev/null +++ b/ILSpy.Tests/EntityReferenceResolveTests.cs @@ -0,0 +1,66 @@ +// 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.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ICSharpCode.Decompiler.TypeSystem; + +using ILSpy; +using ILSpy.TreeNodes; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +/// +/// EntityReference.Resolve turns an unresolved (module, handle) reference into the live IEntity, +/// the shared resolution used by code-view hover navigation and the assembly-tree node finder. +/// +[TestFixture] +public class EntityReferenceResolveTests +{ + [AvaloniaTest] + public async Task Resolve_Returns_The_Entity_For_A_Module_Handle_Reference() + { + var (_, vm) = await TestHarness.BootAsync(); + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + var entity = (IEntity)typeNode.Member!; + var module = entity.ParentModule!.MetadataFile!; + + var reference = new EntityReference(module, entity.MetadataToken); + var resolved = reference.Resolve(vm.AssemblyTreeModel.AssemblyList!); + + resolved.Should().NotBeNull("the (module, handle) pair resolves back to its entity"); + resolved!.FullName.Should().Be(entity.FullName); + } + + [AvaloniaTest] + public async Task Resolve_Returns_Null_For_An_Unknown_Module() + { + var (_, vm) = await TestHarness.BootAsync(); + // A module path that isn't in the list can't be resolved -> null, not a throw. + var reference = new EntityReference("X:/does/not/exist.dll", + System.Reflection.Metadata.Ecma335.MetadataTokens.TypeDefinitionHandle(2)); + reference.Resolve(vm.AssemblyTreeModel.AssemblyList!).Should().BeNull(); + } +} diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index c516de1a5..950074275 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -598,14 +598,7 @@ namespace ILSpy.AssemblyTree switch (reference) { case EntityReference unresolved: - var module = unresolved.ResolveAssembly(AssemblyList!); - if (module == null) - return null; - var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolved.Handle)); - if (token == null) - return null; - var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached); - return FindTreeNode(typeSystem.MainModule.ResolveEntity(token.Value)); + return FindTreeNode(unresolved.Resolve(AssemblyList!)); case ITypeDefinition type: return FindTypeNode(assemblyListTreeNode, type); diff --git a/ILSpy/EntityReference.cs b/ILSpy/EntityReference.cs index 2303ee067..4da4f1516 100644 --- a/ILSpy/EntityReference.cs +++ b/ILSpy/EntityReference.cs @@ -18,8 +18,10 @@ using System.Diagnostics; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; namespace ILSpy @@ -53,5 +55,24 @@ namespace ILSpy public MetadataFile? ResolveAssembly(AssemblyList context) => metadataFile ?? context.FindAssembly(Module)?.GetMetadataFileOrNull(); + + /// + /// Resolves this reference to a live within , + /// or null when the module, the handle, or the entity can't be resolved. Builds a fresh + /// uncached per call -- the caller is resolving a single + /// handle, not walking the whole module. + /// + public IEntity? Resolve(AssemblyList context) + { + var module = ResolveAssembly(context); + if (module == null) + return null; + var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(Handle)); + if (token == null) + return null; + var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver(), + TypeSystemOptions.Default | TypeSystemOptions.Uncached); + return typeSystem.MainModule.ResolveEntity(token.Value); + } } } diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index c08d3c858..10108869d 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -981,16 +981,7 @@ namespace ILSpy.TextView if (DataContext is not DecompilerTabPageModel m || m.CurrentNode is not { } node) return null; var list = node.AncestorsAndSelf().OfType().FirstOrDefault()?.AssemblyList; - if (list == null) - return null; - var resolvedModule = unresolved.ResolveAssembly(list); - if (resolvedModule == null) - return null; - var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolved.Handle)); - if (token == null) - return null; - var typeSystem = new DecompilerTypeSystem(resolvedModule, resolvedModule.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached); - return typeSystem.MainModule.ResolveEntity(token.Value); + return list == null ? null : unresolved.Resolve(list); default: return null; }