Browse Source

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
pull/3755/head
Siegfried Pammer 4 weeks ago
parent
commit
738ae561dd
  1. 66
      ILSpy.Tests/EntityReferenceResolveTests.cs
  2. 9
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  3. 21
      ILSpy/EntityReference.cs
  4. 11
      ILSpy/TextView/DecompilerTextView.axaml.cs

66
ILSpy.Tests/EntityReferenceResolveTests.cs

@ -0,0 +1,66 @@ @@ -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;
/// <summary>
/// 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.
/// </summary>
[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<TypeTreeNode>(
"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();
}
}

9
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -598,14 +598,7 @@ namespace ILSpy.AssemblyTree @@ -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);

21
ILSpy/EntityReference.cs

@ -18,8 +18,10 @@ @@ -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 @@ -53,5 +55,24 @@ namespace ILSpy
public MetadataFile? ResolveAssembly(AssemblyList context)
=> metadataFile ?? context.FindAssembly(Module)?.GetMetadataFileOrNull();
/// <summary>
/// Resolves this reference to a live <see cref="IEntity"/> within <paramref name="context"/>,
/// or null when the module, the handle, or the entity can't be resolved. Builds a fresh
/// uncached <see cref="DecompilerTypeSystem"/> per call -- the caller is resolving a single
/// handle, not walking the whole module.
/// </summary>
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);
}
}
}

11
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -981,16 +981,7 @@ namespace ILSpy.TextView @@ -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<TreeNodes.AssemblyListTreeNode>().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;
}

Loading…
Cancel
Save