From 9d095705e9c9de05b4ed239d277986bee69c2056 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 7 Jun 2026 22:34:57 +0200 Subject: [PATCH] Resolve assembly, resource, and namespace search results to tree nodes FindTreeNode only handled entity references, so activating an assembly, resource, or namespace search result -- and clicking a resource or namespace link -- silently did nothing. Restore the non-entity cases (LoadedAssembly / MetadataFile / Resource / namespace) the previous version handled. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../FindTreeNodeReferenceTests.cs | 70 +++++++++++++++++++ ILSpy/AssemblyTree/AssemblyTreeModel.cs | 60 ++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 ILSpy.Tests/AssemblyList/FindTreeNodeReferenceTests.cs diff --git a/ILSpy.Tests/AssemblyList/FindTreeNodeReferenceTests.cs b/ILSpy.Tests/AssemblyList/FindTreeNodeReferenceTests.cs new file mode 100644 index 000000000..06cc134c8 --- /dev/null +++ b/ILSpy.Tests/AssemblyList/FindTreeNodeReferenceTests.cs @@ -0,0 +1,70 @@ +// 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 ILSpy.TreeNodes; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +/// +/// AssemblyTreeModel.FindTreeNode must resolve non-entity references -- a whole assembly +/// (LoadedAssembly / MetadataFile) and a namespace -- to their tree nodes, not just entities. +/// (Regression: the Avalonia port only handled entity references, so Assembly/Namespace search +/// results and resource/namespace links silently did nothing.) +/// +[TestFixture] +public class FindTreeNodeReferenceTests +{ + [AvaloniaTest] + public async Task FindTreeNode_Resolves_Assembly_And_Namespace_References() + { + var (_, vm) = await TestHarness.BootAsync(3); + var atm = vm.AssemblyTreeModel; + + var asmNode = atm.FindNode("System.Linq"); + Assert.That(asmNode, Is.Not.Null); + var loaded = asmNode!.LoadedAssembly; + + // LoadedAssembly reference -> its assembly node. (Custom TreeNodeAssertions shadows + // .Should() on tree nodes, so use Assert.That here.) + Assert.That(atm.FindTreeNode(loaded), Is.SameAs(asmNode), + "a LoadedAssembly reference resolves to its assembly node"); + + // MetadataFile reference -> the same assembly node. + var metadataFile = loaded.GetMetadataFileOrNull(); + Assert.That(metadataFile, Is.Not.Null); + Assert.That(atm.FindTreeNode(metadataFile), Is.SameAs(asmNode), + "a MetadataFile reference resolves to its assembly node"); + + // Namespace reference -> its namespace node. + var compilation = loaded.GetTypeSystemOrNull(); + Assert.That(compilation, Is.Not.Null); + var ns = compilation!.MainModule.RootNamespace.GetChildNamespace("System")?.GetChildNamespace("Linq"); + Assert.That(ns, Is.Not.Null); + + Assert.That(atm.FindTreeNode(ns), Is.InstanceOf(), + "a namespace reference resolves to its namespace node"); + } +} diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index 01c49974e..9bf5789de 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -620,11 +620,71 @@ namespace ILSpy.AssemblyTree case IMember member: return FindMemberNode(assemblyListTreeNode, member); + case LoadedAssembly lasm: + return FindAssemblyNode(lasm); + + case MetadataFile metadataFile: + return assemblyListTreeNode.Children.OfType() + .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == metadataFile); + + case Resource resource: + return FindResourceNode(assemblyListTreeNode, resource, null); + + case ValueTuple resourceWithName: + return FindResourceNode(assemblyListTreeNode, resourceWithName.Item1, resourceWithName.Item2); + + case INamespace ns: + return FindNamespaceNode(assemblyListTreeNode, ns); + default: return null; } } + // Resolves a resource (optionally a named sub-entry) to its tree node. Mirrors the previous + // version's AssemblyListTreeNode.FindResourceNode so resource search results / links navigate. + static ILSpyTreeNode? FindResourceNode(AssemblyListTreeNode root, Resource resource, string? name) + { + if (resource == null) + return null; + ILSpyTreeNode? resourceNode = null; + foreach (var node in root.Children.OfType()) + { + if (!node.LoadedAssembly.IsLoaded) + continue; + node.EnsureLazyChildren(); + foreach (var list in node.Children.OfType()) + { + resourceNode = list.Children.OfType().FirstOrDefault(x => x.Resource == resource) + ?? (ILSpyTreeNode?)list.Children.OfType().FirstOrDefault(x => resource.Name.Equals(x.Text)); + if (resourceNode != null) + break; + } + if (resourceNode != null) + break; + } + if (resourceNode == null || name == null || name.Equals(resourceNode.Text)) + return resourceNode; + resourceNode.EnsureLazyChildren(); + return resourceNode.Children.OfType().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. + static NamespaceTreeNode? FindNamespaceNode(AssemblyListTreeNode root, INamespace ns) + { + var module = ns.ContributingModules.FirstOrDefault(); + if (module?.MetadataFile == null) + return null; + var assembly = root.Children.OfType() + .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module.MetadataFile); + if (assembly == null) + return null; + assembly.EnsureLazyChildren(); + return assembly.Children.OfType() + .FirstOrDefault(n => ns.FullName.Length == 0 || ns.FullName.Equals(n.Text)); + } + static TypeTreeNode? FindTypeNode(AssemblyListTreeNode root, ITypeDefinition type) { var module = type.ParentModule?.MetadataFile;