// Copyright (c) 2026 Siegfried Pammer // // 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.Collections.Generic; using System.Linq; 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; namespace ICSharpCode.ILSpy.Tests; /// /// Nested-namespace mode ("Use nested namespace structure"): the tree must expose every namespace, /// and the namespace/type lookup primitives must resolve at any nesting depth. /// [TestFixture] public class NestedNamespaceTreeTests { /// /// Expands an assembly with nested-namespace mode on. The assembly node is expanded first so it /// is a realised, visible row: the filter cascade only runs for children of a visible parent, and /// that cascade is what computes . /// static async Task<(AssemblyTreeModel Model, AssemblyTreeNode Assembly)> BootNestedAsync(string assemblyName) { var (window, vm) = await TestHarness.BootAsync(3); var pane = await window.WaitForComponent(); await pane.WaitForComponent(); AppComposition.Current.GetExport().DisplaySettings.UseNestedNamespaceNodes = true; var assemblyNode = vm.AssemblyTreeModel.FindNode(assemblyName); assemblyNode.IsExpanded = true; assemblyNode.EnsureLazyChildren(); return (vm.AssemblyTreeModel, assemblyNode); } static void ResetNestedMode() => AppComposition.Current.GetExport().DisplaySettings.UseNestedNamespaceNodes = false; static IEnumerable DescendantNamespaces(NamespaceTreeNode node) { yield return node; foreach (var child in node.Children.OfType()) { foreach (var descendant in DescendantNamespaces(child)) yield return descendant; } } [AvaloniaTest] public async Task Nested_Mode_Does_Not_Hide_Namespaces_That_Hold_Only_Child_Namespaces() { // A namespace that contains no types of its own but does contain sub-namespaces -- e.g. // "System.Collections" in an assembly that only ships "System.Collections.Generic" types -- // is a pure intermediate node. It must still be shown: hiding it makes every namespace // underneath it unreachable in the tree, which is what "not all namespaces are showing" // looks like to the user. try { var (_, assemblyNode) = await BootNestedAsync("System.Linq"); var intermediates = assemblyNode.Children.OfType() .SelectMany(DescendantNamespaces) .Where(ns => ns.Children.OfType().Any()) .ToList(); Assert.That(intermediates, Is.Not.Empty, "the fixture assembly must contain at least one namespace with sub-namespaces, " + "otherwise this test asserts nothing"); var hidden = intermediates.Where(ns => ns.IsHidden).Select(ns => ns.FullName).ToList(); Assert.That(hidden, Is.Empty, "namespaces holding sub-namespaces must stay visible; hiding them strands every " + "namespace below them"); } finally { ResetNestedMode(); } } [AvaloniaTest] public async Task Nested_Mode_Interleaves_Types_And_Sub_Namespaces_Alphabetically() { // A namespace holding both types and sub-namespaces lists them as one alphabetical // sequence -- "Collections" (namespace) sits between "Buffers" and "Console" (types) -- // rather than grouping all types ahead of all namespaces. The band is built in a single // pass over the types ordered by full name, and a sub-namespace node is attached when its // first descendant is reached, which lands it at its own alphabetical position. try { var (_, assemblyNode) = await BootNestedAsync(TreeNavigation.CoreLibName); var system = assemblyNode.Children.OfType() .SelectMany(DescendantNamespaces) .Single(ns => ns.FullName == "System"); // Without both kinds present there is no interleaving to observe and the ordering // assertion below would hold vacuously. Assert.That(system.Children.OfType(), Is.Not.Empty, "the fixture's System namespace must declare types of its own"); Assert.That(system.Children.OfType(), Is.Not.Empty, "the fixture's System namespace must contain sub-namespaces"); // ToString() is the ordering key on both node kinds: ReflectionName for a type, // the full dotted path for a namespace. var keys = system.Children.Select(child => child.ToString()).ToList(); Assert.That(keys, Is.Ordered.Using(NaturalStringComparer.Instance), "types and sub-namespaces share one alphabetical sequence; grouping the types " + "ahead of the namespaces breaks the ordering the WPF host had"); } finally { ResetNestedMode(); } } [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() { // FindNamespaceNode is the lookup primitive behind "--navigateto N:..." and namespace // navigation. It takes a full dotted name and must resolve it at any depth -- in nested mode // the node for "System.Collections.Generic" is three levels down, not an assembly child. try { var (_, assemblyNode) = await BootNestedAsync("System.Linq"); var ns = assemblyNode.FindNamespaceNode("System.Collections.Generic"); Assert.That(ns, Is.Not.Null, "a full dotted namespace name must resolve in nested mode, where the matching node " + "is a descendant rather than a direct child of the assembly node"); Assert.That(ns!.FullName, Is.EqualTo("System.Collections.Generic")); Assert.That(ns.Name, Is.EqualTo("Generic"), "in nested mode the display label is the last segment only"); } finally { ResetNestedMode(); } } [AvaloniaTest] public async Task FindTypeNode_Resolves_A_Type_In_A_Nested_Namespace() { // FindTypeNode backs hyperlink clicks, search-result activation and JumpToType. A type whose // namespace has more than one segment must resolve in nested mode too. try { var (_, assemblyNode) = await BootNestedAsync("System.Linq"); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; var typeSystem = (MetadataModule)module.GetTypeSystemOrNull()!.MainModule; var nestedType = typeSystem.TopLevelTypeDefinitions .First(t => t.Namespace.Contains('.') && t.Namespace != "System.Linq"); var node = assemblyNode.FindTypeNode(nestedType); Assert.That(node, Is.Not.Null, $"the tree node for '{nestedType.ReflectionName}' must resolve in nested mode"); Assert.That(node!.Handle, Is.EqualTo((System.Reflection.Metadata.TypeDefinitionHandle)nestedType.MetadataToken)); } finally { ResetNestedMode(); } } [AvaloniaTest] public async Task FindTreeNode_Resolves_A_Type_Reference_In_A_Nested_Namespace() { // The reference-to-node lookup behind hyperlink clicks in the decompiler view, search-result // activation and JumpToType. It goes through TreeNodeLocator rather than calling // AssemblyTreeNode.FindTypeNode directly, so it needs its own coverage in nested mode. try { var (model, assemblyNode) = await BootNestedAsync("System.Linq"); var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull()!; var typeSystem = (MetadataModule)module.GetTypeSystemOrNull()!.MainModule; var nestedType = typeSystem.TopLevelTypeDefinitions .First(t => t.Namespace.Contains('.') && t.Namespace != "System.Linq"); var node = model.FindTreeNode(nestedType); Assert.That(node, Is.InstanceOf(), $"a reference to '{nestedType.ReflectionName}' must resolve to its tree node in nested mode"); Assert.That(((TypeTreeNode)node!).Handle, Is.EqualTo((System.Reflection.Metadata.TypeDefinitionHandle)nestedType.MetadataToken)); } finally { ResetNestedMode(); } } [AvaloniaTest] public async Task Nested_Namespace_Is_Hidden_When_All_Of_Its_Types_Are_Filtered_Out() { // Nested namespace nodes stay subject to the filter cascade: a namespace whose types are all // filtered out by ShowApiLevel must disappear along with them, rather than linger as an empty // row. Opting namespace nodes out of the cascade (by reporting FilterResult.Match) would make // the display bug go away too, but at the cost of this behaviour -- hence the coverage. var settings = AppComposition.Current.GetExport().SessionSettings.LanguageSettings; var original = settings.ShowApiLevel; try { var (_, assemblyNode) = await BootNestedAsync("System.Linq"); // FxResources.* holds only internal resource-string types, so PublicOnly empties it. var fxResources = assemblyNode.Children.OfType() .SingleOrDefault(ns => ns.FullName == "FxResources"); Assert.That(fxResources, Is.Not.Null, "the fixture assembly must expose the non-public FxResources namespace in nested mode"); Assert.That(fxResources!.IsHidden, Is.False, "it is visible while every API level is shown"); settings.ShowApiLevel = ApiVisibility.PublicOnly; assemblyNode.RefreshRealizedFilter(); Assert.That(fxResources.IsHidden, Is.True, "a namespace left with no visible types must be hidden by the filter cascade"); settings.ShowApiLevel = ApiVisibility.All; assemblyNode.RefreshRealizedFilter(); Assert.That(fxResources.IsHidden, Is.False, "and it must come back when the API level is widened again"); } finally { settings.ShowApiLevel = original; ResetNestedMode(); } } }