Browse Source

Resolve navigate-to ID strings at the metadata level

FindEntityInRelevantAssemblies now uses the metadata-level FindEntity
instead of parsing the ID string into type-system references and
resolving them per assembly. Two behaviors of the old path need
explicit handling because FindEntity only searches the modules it is
handed: reference assemblies are skipped so the search prefers an
assembly with a usable definition, and a member whose declaring type is
present only as a type forwarder is looked up in the assembly the
forwarder points to, which the assembly resolver may load on demand
(the old path got this through DecompilerTypeSystem resolution).

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3941/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
8473e821d3
  1. 85
      ILSpy.Tests/AssemblyTree/NavigateToForwardedMemberTests.cs
  2. 107
      ILSpy/AssemblyTree/AssemblyTreeModel.cs

85
ILSpy.Tests/AssemblyTree/NavigateToForwardedMemberTests.cs

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
// Copyright (c) 2026 Siegfried Pammer
//
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using AwesomeAssertions;
using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpyX;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.AssemblyTree;
/// <summary>
/// A member ID whose declaring type reaches the opened assembly only as a type forwarder:
/// the ID names System.String, but a facade like System.Runtime.dll carries no type rows at
/// all - the member lives in the assembly the forwarder points at, and the resolver has to
/// follow it there. This is what <c>--navigateto</c> hits on any modern framework assembly.
/// </summary>
[TestFixture]
public class NavigateToForwardedMemberTests
{
static string FacadePath => Path.Combine(
Path.GetDirectoryName(typeof(object).Assembly.Location)!, "System.Runtime.dll");
[Test]
public async Task NavigateTo_Follows_A_Type_Forwarder_Into_The_Assembly_Holding_The_Member()
{
string facadePath = FacadePath;
File.Exists(facadePath).Should().BeTrue(
"the running framework must ship the System.Runtime facade this test navigates through");
var assemblyList = new AssemblyList();
var facade = assemblyList.OpenAssembly(facadePath);
var facadeFile = await facade.GetMetadataFileOrNullAsync();
facadeFile.Should().NotBeNull();
// Guard the premise: if the facade defined System.String itself, the plain lookup
// would answer and the forwarder path under test would never run.
facadeFile!.Metadata.TypeDefinitions
.Select(h => facadeFile.Metadata.GetString(facadeFile.Metadata.GetTypeDefinition(h).Name))
.Should().NotContain("String", "System.Runtime is a facade of forwarders, not definitions");
facadeFile.Metadata.ExportedTypes.Should().NotBeEmpty("the facade forwards its types");
var entity = AssemblyTreeModel.FindEntityInRelevantAssemblies(
"M:System.String.Concat(System.String,System.String)", new[] { facade });
entity.Should().NotBeNull("the ID must resolve through the forwarder");
entity!.Name.Should().Be("Concat");
entity.DeclaringType!.FullName.Should().Be("System.String");
entity.ParentModule!.MetadataFile!.FileName.Should().NotBe(facadePath,
"the member rows live in the forwarder's target assembly, not in the facade");
}
[Test]
public async Task NavigateTo_Returns_Null_For_A_Member_That_No_Forwarder_Leads_To()
{
var assemblyList = new AssemblyList();
var facade = assemblyList.OpenAssembly(FacadePath);
await facade.GetMetadataFileOrNullAsync();
var entity = AssemblyTreeModel.FindEntityInRelevantAssemblies(
"M:System.String.ThisMethodDoesNotExist", new[] { facade });
entity.Should().BeNull("an unresolvable member must not resolve to some other member");
}
}

107
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -34,7 +34,6 @@ using ICSharpCode.Decompiler; @@ -34,7 +34,6 @@ using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Documentation;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView;
@ -781,63 +780,67 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -781,63 +780,67 @@ namespace ICSharpCode.ILSpy.AssemblyTree
internal static IEntity? FindEntityInRelevantAssemblies(string navigateTo, IEnumerable<LoadedAssembly> relevantAssemblies)
{
ITypeReference typeRef;
IMemberReference? memberRef = null;
if (navigateTo.StartsWith("T:", StringComparison.Ordinal))
{
typeRef = IdStringProvider.ParseTypeName(navigateTo);
}
else
{
memberRef = IdStringProvider.ParseMemberIdString(navigateTo);
typeRef = memberRef.DeclaringTypeReference;
}
foreach (var asm in relevantAssemblies)
{
var module = asm.GetMetadataFileOrNull();
if (module != null && CanResolveTypeInPEFile(module, typeRef, out var typeHandle))
{
ICompilation compilation = typeHandle.Kind == HandleKind.ExportedType
? new DecompilerTypeSystem(module, module.GetAssemblyResolver())
: new SimpleCompilation((PEFile)module, MinimalCorlib.Instance);
return memberRef == null
? typeRef.Resolve(new SimpleTypeResolveContext(compilation)) as ITypeDefinition
: memberRef.Resolve(new SimpleTypeResolveContext(compilation));
}
}
return null;
// Reference assemblies are skipped so the search keeps looking for another
// assembly that might have a usable definition.
IReadOnlyList<MetadataFile> modules = [.. from asm in relevantAssemblies let mod = asm.GetMetadataFileOrNull() where mod != null && !mod.IsReferenceAssembly() select mod];
var (module, handle) = IdStringProvider.FindEntity(navigateTo, modules);
if (module == null || handle.IsNil)
(module, handle) = FindMemberViaTypeForwarders(navigateTo, modules);
if (module == null || handle.IsNil)
return null;
var metadataModule = module.GetLoadedAssembly().GetTypeSystemOrNull()?.MainModule as MetadataModule;
if (metadataModule == null)
return null;
return metadataModule.ResolveEntity(handle);
}
static bool CanResolveTypeInPEFile(MetadataFile module, ITypeReference typeRef, out EntityHandle typeHandle)
/// <summary>
/// A member ID whose declaring type is present in the given modules only as a type
/// forwarder cannot be found by <see cref="IdStringProvider.FindEntity"/> alone:
/// the member rows live in the assembly the forwarder points to. Resolve that
/// assembly and search the member there.
/// </summary>
static (MetadataFile? Module, EntityHandle Handle) FindMemberViaTypeForwarders(string navigateTo, IReadOnlyList<MetadataFile> modules)
{
// Reference assemblies are skipped so the loop keeps looking for an actual definition.
if (module.IsReferenceAssembly())
if (navigateTo.Length < 2 || navigateTo[1] != ':' || navigateTo.StartsWith("T:", StringComparison.Ordinal))
return default;
int parenPos = navigateTo.IndexOf('(');
if (parenPos < 0)
parenPos = navigateTo.LastIndexOf('~');
if (parenPos < 0)
parenPos = navigateTo.Length;
int dotPos = navigateTo.LastIndexOf('.', parenPos - 1);
if (dotPos <= 2)
return default;
string declaringTypeId = "T:" + navigateTo[2..dotPos];
// Forwarder chains are short; the bound only guards against cycles.
for (int depth = 0; depth < 16; depth++)
{
typeHandle = default;
return false;
var (module, typeHandle) = IdStringProvider.FindEntity(declaringTypeId, modules);
if (module == null || typeHandle.Kind != HandleKind.ExportedType)
return default;
var target = ResolveForwarderTarget(module, (ExportedTypeHandle)typeHandle);
if (target == null)
return default;
modules = [target];
var result = IdStringProvider.FindEntity(navigateTo, modules);
if (!result.Handle.IsNil)
return result;
}
return default;
}
switch (typeRef)
{
case GetPotentiallyNestedClassTypeReference topLevelType:
typeHandle = topLevelType.ResolveInPEFile(module);
return !typeHandle.IsNil;
case NestedTypeReference nestedType:
if (!CanResolveTypeInPEFile(module, nestedType.DeclaringTypeReference, out typeHandle))
return false;
if (typeHandle.Kind == HandleKind.ExportedType)
return true;
var typeDef = module.Metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle);
typeHandle = typeDef.GetNestedTypes().FirstOrDefault(t => {
var td = module.Metadata.GetTypeDefinition(t);
var typeName = ReflectionHelper.SplitTypeParameterCountFromReflectionName(module.Metadata.GetString(td.Name), out int typeParameterCount);
return nestedType.AdditionalTypeParameterCount == typeParameterCount && nestedType.Name == typeName;
});
return !typeHandle.IsNil;
default:
typeHandle = default;
return false;
}
static MetadataFile? ResolveForwarderTarget(MetadataFile module, ExportedTypeHandle handle)
{
var metadata = module.Metadata;
var implementation = metadata.GetExportedType(handle).Implementation;
// Nested forwarded types point at their enclosing forwarder entry.
while (implementation.Kind == HandleKind.ExportedType)
implementation = metadata.GetExportedType((ExportedTypeHandle)implementation).Implementation;
if (implementation.Kind != HandleKind.AssemblyReference)
return null;
var assemblyReference = new Decompiler.Metadata.AssemblyReference(module, (AssemblyReferenceHandle)implementation);
return module.GetAssemblyResolver().Resolve(assemblyReference);
}
void LoadAssemblies(IEnumerable<string> fileNames, List<LoadedAssembly>? loadedAssemblies = null, bool focusNode = true)

Loading…
Cancel
Save