Browse Source

Reach package-nested nodes from every lookup, and only along one path

Resolving a metadata file to its assembly node learned to descend into packages,
but three sibling lookups kept their own scan of the root's direct children, so
a token reference, a metadata:// link and a LoadedAssembly reference still
resolved to nothing inside a package. One of them sat behind a guard whose
result was never used, which returned early for exactly the case it was meant to
serve. Routing all of them through the one lookup fixes them together.

Namespaces were matched by comparing a full name against a node label, which is
only ever equal in flat mode: with nested namespace nodes the label is the last
segment, and the empty-name test matched the first child rather than the global
namespace node. The assembly node already indexes its namespaces by full name.

The descent itself no longer sweeps the package depth-first. Expanding a folder
resolves and extracts every .dll it holds, so the path is taken from the
package's folder graph, which costs no tree node and reads no entry.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4025/head
Siegfried Pammer 4 weeks ago
parent
commit
482c0bd81b
  1. 41
      ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs
  2. 76
      ILSpy/AssemblyTree/TreeNodeLocator.cs
  3. 11
      ILSpy/Metadata/MetadataNavigator.cs
  4. 11
      ILSpy/Metadata/MetadataProtocolHandler.cs

41
ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs

@ -149,24 +149,43 @@ public class AssemblyTreeModelTests @@ -149,24 +149,43 @@ public class AssemblyTreeModelTests
// Search enumerates the contents of packages whether or not the user ever opened them in
// the tree, so activating such a result has to reach a node that does not exist yet.
var (_, vm) = await TestHarness.BootAsync();
await vm.OpenAssemblyAsync(CreatePackage());
var nested = (await vm.AssemblyTreeModel.AssemblyList!.GetAllAssemblies())
.Single(a => a.FileName == "lib/net10.0/Nested.dll");
var type = nested.GetTypeSystemOrNull()!.MainModule.TypeDefinitions
.Single(t => t.Name == FixtureAssembly.TypeName);
var node = vm.AssemblyTreeModel.FindTreeNode(type);
var tempDir = Path.Combine(Path.GetTempPath(), "ILSpy.Tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
var zipPath = Path.Combine(tempDir, "package.zip");
using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create))
zip.CreateEntryFromFile(FixtureAssembly.Emit("Nested"), "lib/net10.0/Nested.dll");
// Assert the owning module, not just the node type: the fixture's type handle is
// 0x02000002, which resolves in nearly every assembly on the list, so a lookup that fell
// back to the first top-level node would still hand back some TypeTreeNode.
((object?)node).Should().BeOfType<TypeTreeNode>()
.Which.Module.Should().BeSameAs(nested.GetMetadataFileOrNull(),
"the lookup must descend into the package's folders, expanding them on the way.");
}
await vm.OpenAssemblyAsync(zipPath);
[AvaloniaTest]
public async Task FindTreeNode_leaves_package_folders_off_the_path_unexpanded()
{
// Expanding a package folder resolves and extracts every .dll it holds, so a lookup that
// swept the package depth-first would pay for entries the user never asked about.
var (_, vm) = await TestHarness.BootAsync();
var package = await vm.OpenAssemblyAsync(CreatePackage());
var nested = (await vm.AssemblyTreeModel.AssemblyList!.GetAllAssemblies())
.Single(a => a.ParentBundle != null);
.Single(a => a.FileName == "lib/net10.0/Nested.dll");
var type = nested.GetTypeSystemOrNull()!.MainModule.TypeDefinitions
.Single(t => t.Name == FixtureAssembly.TypeName);
var node = vm.AssemblyTreeModel.FindTreeNode(type);
((object?)vm.AssemblyTreeModel.FindTreeNode(type)).Should().NotBeNull();
// Cast through object so the generic Should() resolves, not the SharpTreeNode shadow.
((object?)node).Should().BeOfType<TypeTreeNode>(
"the lookup must descend into the package's folders, expanding them on the way");
var packageNode = vm.AssemblyTreeModel.FindAssemblyNode(package);
((object?)packageNode).Should().NotBeNull();
var sibling = packageNode!.Children.OfType<PackageFolderTreeNode>()
.Single(f => f.Text as string == "runtimes/win-x64");
sibling.Children.Should().BeEmpty(
"only the folders on the path down to the target get expanded.");
}
}

76
ILSpy/AssemblyTree/TreeNodeLocator.cs

@ -97,7 +97,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -97,7 +97,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree
return FindMemberNode(root, member);
case LoadedAssembly lasm:
return root.FindAssemblyNode(lasm);
return FindAssemblyNode(root, lasm);
case MetadataFile metadataFile:
return FindAssemblyNode(root, metadataFile);
@ -122,13 +122,15 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -122,13 +122,15 @@ namespace ICSharpCode.ILSpy.AssemblyTree
/// down, so this resolves even when the user has never opened the package in the tree.
/// </summary>
public static AssemblyTreeNode? FindAssemblyNode(AssemblyListTreeNode root, MetadataFile? module)
=> FindAssemblyNode(root, module?.GetLoadedAssemblyOrNull());
/// <inheritdoc cref="FindAssemblyNode(AssemblyListTreeNode, MetadataFile?)"/>
public static AssemblyTreeNode? FindAssemblyNode(AssemblyListTreeNode root, LoadedAssembly? assembly)
{
// A package child records the bundle it came from, so walking up that chain leads
// straight to the one top-level node worth descending into. Searching the tree for a
// matching module instead would have to visit every namespace and type node already
// built, and would still miss nested assemblies that are not loaded yet.
// A package child records the bundle it came from, so walking up that chain names every
// package to descend into, outermost first, ending at the one top-level node.
var nesting = new Stack<LoadedAssembly>();
for (var current = module?.GetLoadedAssemblyOrNull(); current != null; current = current.ParentBundle)
for (var current = assembly; current != null; current = current.ParentBundle)
nesting.Push(current);
if (nesting.Count == 0)
return null;
@ -139,22 +141,49 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -139,22 +141,49 @@ namespace ICSharpCode.ILSpy.AssemblyTree
return node;
}
// Depth-first search for one assembly within a package node's folder structure. Only
// package folders are descended into, so the walk stays inside the package.
static AssemblyTreeNode? FindNestedAssemblyNode(SharpTreeNode packageNode, LoadedAssembly assembly)
// Finds one assembly inside a package node, expanding only the folders on the path down to
// it. Expanding a package folder resolves and extracts every .dll/.exe it holds, so the
// path is taken from the package's in-memory folder graph first -- that costs no tree node
// and reads no package entry.
static AssemblyTreeNode? FindNestedAssemblyNode(AssemblyTreeNode packageNode, LoadedAssembly assembly)
{
packageNode.EnsureLazyChildren();
foreach (var child in packageNode.Children)
var rootFolder = packageNode.LoadedAssembly.GetLoadResultAsync().GetAwaiter().GetResult().Package?.RootFolder;
if (rootFolder == null)
return null;
var path = new HashSet<PackageFolder>();
if (!rootFolder.HasResolved(assembly) && !CollectFolderPath(rootFolder, assembly, path))
return null;
SharpTreeNode node = packageNode;
while (true)
{
switch (child)
node.EnsureLazyChildren();
if (node.Children.OfType<AssemblyTreeNode>().FirstOrDefault(n => n.LoadedAssembly == assembly) is { } nested)
return nested;
// A folder node stands for the deepest link of a collapsed single-child chain
// (a/b/c shows as one node), so match its folder against the whole path rather
// than against one expected next segment.
if (node.Children.OfType<PackageFolderTreeNode>().FirstOrDefault(f => path.Contains(f.Folder)) is not { } next)
return null;
node = next;
}
}
// Fills <paramref name="path"/> with the folders between <paramref name="folder"/> (exclusive)
// and the one that already resolved <paramref name="assembly"/>. Every package-nested
// LoadedAssembly is produced by exactly one PackageFolder.ResolveEntry call, so the owning
// folder's resolution cache is what identifies it.
static bool CollectFolderPath(PackageFolder folder, LoadedAssembly assembly, HashSet<PackageFolder> path)
{
foreach (var subFolder in folder.Folders)
{
if (subFolder.HasResolved(assembly) || CollectFolderPath(subFolder, assembly, path))
{
case AssemblyTreeNode nested when nested.LoadedAssembly == assembly:
return nested;
case PackageFolderTreeNode folder when FindNestedAssemblyNode(folder, assembly) is { } found:
return found;
path.Add(subFolder);
return true;
}
}
return null;
return false;
}
// Resolves a resource (optionally a named sub-entry) to its tree node. Mirrors the previous
@ -185,19 +214,16 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -185,19 +214,16 @@ namespace ICSharpCode.ILSpy.AssemblyTree
return resourceNode.Children.OfType<ILSpyTreeNode>().FirstOrDefault(x => name.Equals(x.Text)) ?? resourceNode;
}
// Resolves a namespace to its tree node within its contributing assembly. Mirrors the previous
// version's AssemblyListTreeNode.FindNamespaceNode.
// Resolves a namespace to its tree node within its contributing assembly.
static NamespaceTreeNode? FindNamespaceNode(AssemblyListTreeNode root, INamespace ns)
{
var module = ns.ContributingModules.FirstOrDefault();
if (module?.MetadataFile == null)
return null;
var assembly = FindAssemblyNode(root, module.MetadataFile);
if (assembly == null)
return null;
assembly.EnsureLazyChildren();
return assembly.Children.OfType<NamespaceTreeNode>()
.FirstOrDefault(n => ns.FullName.Length == 0 || ns.FullName.Equals(n.Text));
// The assembly node indexes every namespace it built by full, unescaped name. Matching
// against the node labels instead would fail in nested-namespace mode, where the node
// for "A.B.C" is a descendant and its label is only the last segment.
return FindAssemblyNode(root, module.MetadataFile)?.FindNamespaceNode(ns.FullName);
}
public static TypeTreeNode? FindTypeNode(AssemblyListTreeNode root, ITypeDefinition type)

11
ILSpy/Metadata/MetadataNavigator.cs

@ -71,10 +71,6 @@ namespace ICSharpCode.ILSpy.Metadata @@ -71,10 +71,6 @@ namespace ICSharpCode.ILSpy.Metadata
if (!TryReadRow(row, "Token", out var file, out var handle) || handle.IsNil)
return null;
var owningAssembly = assemblyTreeModel.AssemblyList?.GetAssemblies()
.FirstOrDefault(a => ReferenceEquals(a.GetMetadataFileOrNull(), file));
if (owningAssembly is null)
return null;
if (file?.GetTypeSystemWithCurrentOptionsOrNull()?.MainModule is not MetadataModule metadataModule)
return null;
IEntity? entity;
@ -93,10 +89,9 @@ namespace ICSharpCode.ILSpy.Metadata @@ -93,10 +89,9 @@ namespace ICSharpCode.ILSpy.Metadata
/// </summary>
public MetadataTableTreeNode? FindTableNode(MetadataTokenReference reference)
{
var targetAssembly = assemblyTreeModel.Root?.Children
.OfType<AssemblyTreeNode>()
.FirstOrDefault(a => ReferenceEquals(a.LoadedAssembly.GetMetadataFileOrNull(), reference.MetadataFile));
if (targetAssembly == null)
// FindTreeNode, not a scan of the root's children: the module may sit inside a package
// or bundle, whose assembly nodes are grandchildren of a folder node.
if (assemblyTreeModel.FindTreeNode(reference.MetadataFile) is not AssemblyTreeNode targetAssembly)
return null;
targetAssembly.EnsureLazyChildren();
var metaNode = targetAssembly.Children.OfType<MetadataTreeNode>().FirstOrDefault();

11
ILSpy/Metadata/MetadataProtocolHandler.cs

@ -52,13 +52,10 @@ namespace ICSharpCode.ILSpy.Metadata @@ -52,13 +52,10 @@ namespace ICSharpCode.ILSpy.Metadata
newTabPage = true;
if (protocol != "metadata")
return null;
// 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)
// FindTreeNode resolves a MetadataFile to its assembly node, including one nested in a
// package or bundle, where the node is a grandchild of a folder node rather than a
// child of the root.
if (assemblyTreeModel.FindTreeNode(module) is not AssemblyTreeNode assemblyNode)
return null;
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().FirstOrDefault();

Loading…
Cancel
Save