diff --git a/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs b/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs index 7db035305..a02bd333f 100644 --- a/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs +++ b/ICSharpCode.ILSpyX/LoadedAssemblyExtensions.cs @@ -70,6 +70,20 @@ namespace ICSharpCode.ILSpyX return GetLoadedAssembly(file).GetTypeSystemOrNull(DecompilerTypeSystem.GetOptions(settings)); } + /// + /// Like , but returns null for a file that was built outside + /// the machinery instead of throwing. + /// + public static LoadedAssembly? GetLoadedAssemblyOrNull(this MetadataFile file) + { + if (file == null) + throw new ArgumentNullException(nameof(file)); + lock (LoadedAssembly.loadedAssemblies) + { + return LoadedAssembly.loadedAssemblies.TryGetValue(file, out var loadedAssembly) ? loadedAssembly : null; + } + } + public static LoadedAssembly GetLoadedAssembly(this MetadataFile file) { if (file == null) diff --git a/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs b/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs index d0a44536d..ceda979af 100644 --- a/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs +++ b/ILSpy.Tests/AssemblyTree/AssemblyTreeModelTests.cs @@ -16,6 +16,12 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; + using Avalonia.Headless.NUnit; using AwesomeAssertions; @@ -24,6 +30,7 @@ using ICSharpCode.ILSpyX; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AssemblyTree; +using ICSharpCode.ILSpy.TreeNodes; using NUnit.Framework; @@ -63,4 +70,31 @@ public class AssemblyTreeModelTests model.ActiveListName.Should().Be(AssemblyListManager.DefaultListName, "Initialize selects the (Default) list so the tree has something to render at startup."); } + + [AvaloniaTest] + public async Task FindTreeNode_resolves_a_type_inside_an_unexpanded_package() + { + // 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(); + + 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"); + + await vm.OpenAssemblyAsync(zipPath); + + var nested = (await vm.AssemblyTreeModel.AssemblyList!.GetAllAssemblies()) + .Single(a => a.ParentBundle != null); + var type = nested.GetTypeSystemOrNull()!.MainModule.TypeDefinitions + .Single(t => t.Name == FixtureAssembly.TypeName); + + var node = vm.AssemblyTreeModel.FindTreeNode(type); + + // Cast through object so the generic Should() resolves, not the SharpTreeNode shadow. + ((object?)node).Should().BeOfType( + "the lookup must descend into the package's folders, expanding them on the way"); + } } diff --git a/ILSpy/AssemblyTree/TreeNodeLocator.cs b/ILSpy/AssemblyTree/TreeNodeLocator.cs index 693fe68ba..3b2fc49a0 100644 --- a/ILSpy/AssemblyTree/TreeNodeLocator.cs +++ b/ILSpy/AssemblyTree/TreeNodeLocator.cs @@ -100,8 +100,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree return root.FindAssemblyNode(lasm); case MetadataFile metadataFile: - return root.Children.OfType() - .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == metadataFile); + return FindAssemblyNode(root, metadataFile); case Resource resource: return FindResourceNode(root, resource, null); @@ -117,6 +116,47 @@ namespace ICSharpCode.ILSpy.AssemblyTree } } + /// + /// Finds the node for the assembly was loaded from, including + /// assemblies nested inside a package or bundle. Package folders are expanded on the way + /// down, so this resolves even when the user has never opened the package in the tree. + /// + public static AssemblyTreeNode? FindAssemblyNode(AssemblyListTreeNode root, MetadataFile? module) + { + // 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. + var nesting = new Stack(); + for (var current = module?.GetLoadedAssemblyOrNull(); current != null; current = current.ParentBundle) + nesting.Push(current); + if (nesting.Count == 0) + return null; + + var node = root.FindAssemblyNode(nesting.Pop()); + while (node != null && nesting.Count > 0) + node = FindNestedAssemblyNode(node, nesting.Pop()); + 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) + { + packageNode.EnsureLazyChildren(); + foreach (var child in packageNode.Children) + { + switch (child) + { + case AssemblyTreeNode nested when nested.LoadedAssembly == assembly: + return nested; + case PackageFolderTreeNode folder when FindNestedAssemblyNode(folder, assembly) is { } found: + return found; + } + } + 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) @@ -152,8 +192,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree var module = ns.ContributingModules.FirstOrDefault(); if (module?.MetadataFile == null) return null; - var assembly = root.Children.OfType() - .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module.MetadataFile); + var assembly = FindAssemblyNode(root, module.MetadataFile); if (assembly == null) return null; assembly.EnsureLazyChildren(); @@ -166,8 +205,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree var module = type.ParentModule?.MetadataFile; if (module == null) return null; - var assembly = root.Children.OfType() - .FirstOrDefault(a => a.LoadedAssembly.GetMetadataFileOrNull() == module); + var assembly = FindAssemblyNode(root, module); if (assembly == null) return null;