From 46e2df4b10df883525c52ed60fd2b772588e10e6 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 7 May 2026 12:49:37 +0200 Subject: [PATCH] Namespace IsPublicAPI aggregates children + filter override Mirrors the WPF AssemblyTreeNode.SetPublicAPI recursive walk: a namespace counts as public-API iff at least one type it contains is public-API. Inline on NamespaceTreeNode itself as a lazy-evaluated cached aggregate (types don't change accessibility at runtime), so the cell template's gray-foreground binding picks up the right value on first render. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../Options/ApiVisibilityFilterTests.cs | 16 +++++++++++++ ILSpy/TreeNodes/NamespaceTreeNode.cs | 24 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs b/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs index 821de8824..ece9d04c6 100644 --- a/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs +++ b/ILSpy.Tests/Options/ApiVisibilityFilterTests.cs @@ -65,6 +65,22 @@ public class ApiVisibilityFilterTests var (privateNode, privateMethod) = FindNonPublicMethodInLoadedAssemblies(vm); privateNode.IsPublicAPI.Should().BeFalse( $"{privateMethod.DeclaringType?.Name}.{privateMethod.Name} is non-public"); + + // Assembly + root nodes inherit ILSpyTreeNode's true default — no accessibility to + // weigh, so they never pick up the gray non-public styling. Without this assertion, + // flipping the base default to false silently passes everything else even though + // every assembly row would render gray (mutation-test hole, May 2026). + var assemblyNode = vm.AssemblyTreeModel.FindNode( + typeof(System.Linq.Enumerable).Assembly.GetName().Name!); + assemblyNode.IsPublicAPI.Should().BeTrue( + "AssemblyTreeNode must inherit the true default"); + + // Namespaces aggregate from their children — System.Linq holds public types so its + // namespace node reports IsPublicAPI=true. + var publicNamespace = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq"); + publicNamespace.IsPublicAPI.Should().BeTrue( + "a namespace whose children include public types is itself public-API"); } [AvaloniaTest] diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index 16fdd82eb..0b860472a 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -24,6 +24,7 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpyX; +using ILSpy; using ILSpy.Languages; namespace ILSpy.TreeNodes @@ -78,5 +79,28 @@ namespace ILSpy.TreeNodes .Where(t => t.Namespace == name && t.DeclaringTypeDefinition == null); language.DecompileNamespace(name, types, output, options); } + + // A namespace counts as public-API iff at least one type it contains is public-API. + // Forces lazy children once so the aggregate is available before the cell template + // queries it for the gray-foreground binding; the result is cached because types + // don't change accessibility at runtime. Mirrors WPF's AssemblyTreeNode.SetPublicAPI + // recursive walk. + bool? cachedIsPublicAPI; + public override bool IsPublicAPI { + get { + if (cachedIsPublicAPI is { } cached) + return cached; + EnsureLazyChildren(); + cachedIsPublicAPI = Children.OfType().Any(c => c.IsPublicAPI); + return cachedIsPublicAPI.Value; + } + } + + public override FilterResult Filter(LanguageSettings settings) + { + if (settings.ShowApiLevel == ApiVisibility.PublicOnly && !IsPublicAPI) + return FilterResult.Hidden; + return FilterResult.Match; + } } }