From c40c299ff83f318158f687929e4ab3f9ef6ecdb8 Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:18:11 +0200 Subject: [PATCH] Read the full namespace path off tree namespace nodes, not the label In nested-namespace mode a NamespaceTreeNode's display label is only its last segment ("Generic"), while the full dotted path ("System.Collections.Generic") is what identifies the namespace in metadata and to the docs site. Three call sites read the label where they need the full path, so in nested mode each targets the wrong namespace: decompiling a namespace node queries an empty one and titles the output after the last segment; the MSDN URL points at the wrong page; and scope-search-to-namespace scopes to the wrong name. These are ds5678's fixes from #3879, reintegrated on top of the eager namespace rebuild. #3879's other Name -> FullName corrections, in AssemblyTreeNode.FindNamespaceNode and TreeNodeLocator, are already covered here by the full-namespace-name and type-handle indexes, so only the Decompile and search-entry cases carry over. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../AssemblyList/NestedNamespaceTreeTests.cs | 32 +++++++++++++++++++ ILSpy/Commands/SearchMsdnContextMenuEntry.cs | 6 ++-- .../ScopeSearchToNamespaceContextMenuEntry.cs | 2 +- ILSpy/TreeNodes/NamespaceTreeNode.cs | 4 +-- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/ILSpy.Tests/AssemblyList/NestedNamespaceTreeTests.cs b/ILSpy.Tests/AssemblyList/NestedNamespaceTreeTests.cs index 2bf7c53a9..f41b9b03b 100644 --- a/ILSpy.Tests/AssemblyList/NestedNamespaceTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/NestedNamespaceTreeTests.cs @@ -22,12 +22,14 @@ using System.Threading.Tasks; using Avalonia.Headless.NUnit; +using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AssemblyTree; +using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.TreeNodes; using NUnit.Framework; @@ -143,6 +145,36 @@ public class NestedNamespaceTreeTests } } + [AvaloniaTest] + public async Task Decompiling_A_Nested_Namespace_Uses_Its_Full_Path_Not_The_Display_Label() + { + // In nested mode a NamespaceTreeNode's display label is only its last segment ("Generic"), + // while the metadata it has to query -- and the name it titles the output with -- is the full + // path ("System.Collections.Generic"). Decompiling by the label queries the wrong namespace, + // usually an empty one, and titles the output after the wrong name. + + try + { + var (_, assemblyNode) = await BootNestedAsync(TreeNavigation.CoreLibName); + + var generic = assemblyNode.Children.OfType() + .SelectMany(DescendantNamespaces) + .Single(ns => ns.FullName == "System.Collections.Generic"); + + var language = AppComposition.Current.GetExport() + .Languages.OfType().First(); + var output = new PlainTextOutput(); + generic.Decompile(language, output, new DecompilationOptions(new DecompilerSettings())); + + Assert.That(output.ToString(), Does.Contain("System.Collections.Generic"), + "the namespace is decompiled and titled by its full path, not the 'Generic' display label"); + } + finally + { + ResetNestedMode(); + } + } + [AvaloniaTest] public async Task FindNamespaceNode_Resolves_A_Nested_Namespace() { diff --git a/ILSpy/Commands/SearchMsdnContextMenuEntry.cs b/ILSpy/Commands/SearchMsdnContextMenuEntry.cs index 8c025973b..062ccdf08 100644 --- a/ILSpy/Commands/SearchMsdnContextMenuEntry.cs +++ b/ILSpy/Commands/SearchMsdnContextMenuEntry.cs @@ -72,8 +72,8 @@ namespace ICSharpCode.ILSpy.Commands { switch (node) { - case NamespaceTreeNode ns when !string.IsNullOrEmpty(ns.Name): - return (MsdnPrefix + ns.Name).ToLowerInvariant(); + case NamespaceTreeNode ns when !string.IsNullOrEmpty(ns.FullName): + return (MsdnPrefix + ns.FullName).ToLowerInvariant(); case IMemberTreeNode m when m.Member is { } entity: // Reflection-name → docs path: backticks (generic arity) become hyphens, // `+` (nested type) becomes `.`, and ".ctor" gets a "-ctor" tail because @@ -96,7 +96,7 @@ namespace ICSharpCode.ILSpy.Commands switch (node) { case NamespaceTreeNode ns: - return !string.IsNullOrEmpty(ns.Name); + return !string.IsNullOrEmpty(ns.FullName); case IMemberTreeNode m when m.Member is { } entity: if (!IsExternallyVisible(entity.Accessibility)) return false; diff --git a/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs b/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs index a01ac7610..44e1653a8 100644 --- a/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs +++ b/ILSpy/Search/ScopeSearchToNamespaceContextMenuEntry.cs @@ -60,7 +60,7 @@ namespace ICSharpCode.ILSpy.Search static string? Namespace(TextViewContext context) { if (context.SelectedTreeNodes is { Length: > 0 } nodes && nodes.All(n => n is NamespaceTreeNode)) - return nodes.OfType().FirstOrDefault()?.Name; + return nodes.OfType().FirstOrDefault()?.FullName; return (context.Reference?.Reference as IEntity)?.Namespace; } } diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index 2f1d6917d..7915edaff 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -96,8 +96,8 @@ namespace ICSharpCode.ILSpy.TreeNodes return; } var types = typeSystem.MainModule.TypeDefinitions - .Where(t => t.Namespace == name && t.DeclaringTypeDefinition == null); - language.DecompileNamespace(name, types, output, options); + .Where(t => t.Namespace == fullName && t.DeclaringTypeDefinition == null); + language.DecompileNamespace(fullName, types, output, options); } // A namespace counts as public-API iff at least one type it contains is public-API. The