From c4755bbe4d356f0c83ca84301df1811af7f91f49 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 14 May 2026 08:18:52 +0200 Subject: [PATCH] Group global-namespace types under a '-' node The PE TypeDef table includes a pseudo-type plus any user-declared top-level types in the empty namespace. Until now AssemblyTreeNode skipped the empty namespace when building NamespaceTreeNodes and instead hung the global types directly off the assembly node, breaking the long-standing tree shape (every type lives under a namespace folder). --- ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs | 39 +++++++++++++++++++ ILSpy/TreeNodes/AssemblyTreeNode.cs | 16 +++----- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index 47cd57ceb..8de8d6dc5 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -1482,4 +1482,43 @@ public class AssemblyTreeTests command.Invoking(c => c.Execute(null)).Should().NotThrow( "Execute under headless must be a no-op rather than crash"); } + + [AvaloniaTest] + public async Task Global_Namespace_Types_Appear_Under_Dash_Node() + { + // Every PE module carries a pseudo-type in the global (empty) namespace. + // To match the long-standing tree shape, those types must live under a + // NamespaceTreeNode whose Name is the empty string and whose displayed Text is "-", + // NOT as bare TypeTreeNodes hanging directly off the assembly node. + + // Arrange — boot, wait for assemblies, locate CoreLib. + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + + // Assert — exactly one global-namespace node, labelled "-", non-empty contents. + // Plain null check rather than .Should().NotBeNull() because the AwesomeAssertions + // SharpTreeNode-targeted extension hijacks Should() on NamespaceTreeNode? and lacks + // NotBeNull. + var globalNs = assemblyNode.Children.OfType() + .SingleOrDefault(n => n.Name.Length == 0); + Assert.That(globalNs, Is.Not.Null, + "types in the global namespace must be grouped under a NamespaceTreeNode with an empty Name"); + globalNs!.Text.Should().Be("-", + "NamespaceTreeNode renders the empty namespace as '-' to match the long-standing tree shape"); + + // And no bare TypeTreeNodes should hang directly off the assembly — every type must + // route through one of the namespace nodes. + assemblyNode.Children.OfType().Should().BeEmpty( + "global-namespace types must live under the '-' node, not as direct assembly children"); + + globalNs.EnsureLazyChildren(); + globalNs.Children.OfType().Should().NotBeEmpty( + " (and any other global types) belong under the '-' node"); + } } diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 82682e3dc..36506fe7f 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -269,25 +269,19 @@ namespace ILSpy.TreeNodes Children.Add(new ResourceListTreeNode(module)); var metadata = module.Metadata; + // Every top-level namespace string in the module — INCLUDING the empty string for + // types declared at module scope. The empty namespace becomes a NamespaceTreeNode + // whose Text renders as "-"; without that path, global-namespace types (every PE's + // pseudo-type plus any user-declared ones) would have no parent node to + // live under, and the long-standing tree shape would break. var namespaces = metadata.TypeDefinitions .Where(t => metadata.GetTypeDefinition(t).GetDeclaringType().IsNil) .Select(t => metadata.GetString(metadata.GetTypeDefinition(t).Namespace)) - .Where(ns => !string.IsNullOrEmpty(ns)) .Distinct() .OrderBy(ns => ns, NaturalStringComparer.Instance); foreach (var ns in namespaces) Children.Add(new NamespaceTreeNode(ns, module)); - - var globalTypes = metadata.TypeDefinitions - .Where(t => { - var td = metadata.GetTypeDefinition(t); - return td.GetDeclaringType().IsNil - && string.IsNullOrEmpty(metadata.GetString(td.Namespace)); - }); - - foreach (var t in globalTypes) - Children.Add(new TypeTreeNode(t, module)); } public override FilterResult Filter(LanguageSettings settings)