diff --git a/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs b/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs new file mode 100644 index 000000000..119e4aa96 --- /dev/null +++ b/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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.ObjectModel; +using System.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ICSharpCode.ILSpy.AppEnv; +using ICSharpCode.ILSpy.Languages; +using ICSharpCode.ILSpy.Search; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.Extensions; +using ICSharpCode.ILSpyX.Search; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.TreeNodes; + +// LoadedAssembly caches ONE type system per TypeSystemOptions value +// (GetTypeSystemOrNull(options)). The tree, the search, and anything else that resolves +// entities for display must all derive their options from the user's current decompiler +// settings: that keeps them consistent with what decompilation elides AND makes them share +// the single cached instance instead of materialising a second type system per module. +[TestFixture] +public class TypeSystemSharingTests +{ + [AvaloniaTest] + public async Task Tree_nodes_resolve_through_the_settings_derived_type_system() + { + var (_, vm) = await TestHarness.BootAsync(); + var settingsService = AppComposition.Current.GetExport(); + var fixture = await vm.OpenFixtureAsync(); + var typeNode = vm.AssemblyTreeModel.FindNode( + fixture.ShortName, fixture.ShortName, $"{fixture.ShortName}.{FixtureAssembly.TypeName}"); + Assert.That(typeNode, Is.Not.Null); + + var module = fixture.GetMetadataFileOrNull()!; + var expected = module.GetTypeSystemWithDecompilerSettingsOrNull( + settingsService.CreateEffectiveDecompilerSettings()); + + typeNode!.Member!.Compilation.Should().BeSameAs(expected, + "the tree must resolve entities through the cached type system derived from the " + + "current decompiler settings, not a separate default-options one"); + } + + [AvaloniaTest] + public async Task Search_reuses_the_same_cached_type_system_as_the_tree() + { + var (_, vm) = await TestHarness.BootAsync(); + var settingsService = AppComposition.Current.GetExport(); + // A non-default option that changes the derived TypeSystemOptions: a search that + // ignores the live settings materialises a second, default-options type system. + settingsService.DecompilerSettings.NullableReferenceTypes = false; + var fixture = await vm.OpenFixtureAsync(); + var module = fixture.GetMetadataFileOrNull()!; + var language = AppComposition.Current.GetExport().CurrentLanguage; + + var search = new RunningSearch( + new[] { fixture }, FixtureAssembly.TypeName, SearchMode.TypeAndMember, language, + ApiVisibility.PublicAndInternal, new AvaloniaSearchResultFactory(language), + new ObservableCollection(), SearchResult.ComparerByName); + var request = search.BuildRequest(); + + var fromSearch = module.GetTypeSystemWithDecompilerSettingsOrNull(request.DecompilerSettings); + var effective = module.GetTypeSystemWithDecompilerSettingsOrNull( + settingsService.CreateEffectiveDecompilerSettings()); + + fromSearch.Should().BeSameAs(effective, + "the search must derive its type system from the current decompiler settings so it " + + "shares the per-module cache with the tree and respects the user's options"); + } +} diff --git a/ILSpy/ExtensionMethods.cs b/ILSpy/ExtensionMethods.cs new file mode 100644 index 000000000..46f8da5e2 --- /dev/null +++ b/ILSpy/ExtensionMethods.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// 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 ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX; + +namespace ICSharpCode.ILSpy +{ + public static class ExtensionMethods + { + /// + /// Returns the cached type system for derived from the user's + /// current decompiler settings (see + /// ). Everything that + /// resolves entities for display — tree nodes, search, metadata views — should go + /// through this so it agrees with what decompilation produces and shares the + /// per-module type-system cache instead of materialising another instance. Falls back + /// to default settings when composition is unavailable (design-time, minimal test + /// hosts). + /// + public static ICompilation? GetTypeSystemWithCurrentOptionsOrNull(this MetadataFile file) + { + var settings = AppEnv.AppComposition.TryGetExport()?.CreateEffectiveDecompilerSettings() + ?? new Decompiler.DecompilerSettings(); + return file.GetTypeSystemWithDecompilerSettingsOrNull(settings); + } + } +} diff --git a/ILSpy/Metadata/MetadataNavigator.cs b/ILSpy/Metadata/MetadataNavigator.cs index c58bc167f..34680970e 100644 --- a/ILSpy/Metadata/MetadataNavigator.cs +++ b/ILSpy/Metadata/MetadataNavigator.cs @@ -75,7 +75,7 @@ namespace ICSharpCode.ILSpy.Metadata .FirstOrDefault(a => ReferenceEquals(a.GetMetadataFileOrNull(), file)); if (owningAssembly is null) return null; - if (owningAssembly.GetTypeSystemOrNull()?.MainModule is not MetadataModule metadataModule) + if (file?.GetTypeSystemWithCurrentOptionsOrNull()?.MainModule is not MetadataModule metadataModule) return null; IEntity? entity; try diff --git a/ILSpy/Search/RunningSearch.cs b/ILSpy/Search/RunningSearch.cs index cb4d06c57..e59541f19 100644 --- a/ILSpy/Search/RunningSearch.cs +++ b/ILSpy/Search/RunningSearch.cs @@ -254,15 +254,18 @@ namespace ICSharpCode.ILSpy.Search Completed?.Invoke(this); } - SearchRequest BuildRequest() + internal SearchRequest BuildRequest() { var request = ParseInput(searchTerm ?? string.Empty, mode); request.SearchResultFactory = resultFactory; - // MemberSearchStrategy.Search resolves a type system via - // module.GetTypeSystemWithDecompilerSettingsOrNull(request.DecompilerSettings); - // passing null short-circuits to zero results. Default settings are fine for - // search — we only need the type system to materialise. - request.DecompilerSettings = new DecompilerSettings(); + // The search strategies resolve a type system via + // module.GetTypeSystemWithDecompilerSettingsOrNull(request.DecompilerSettings), + // which caches ONE type system per options value on the LoadedAssembly. Derive the + // settings from the user's current options so search hits the same cached instance + // the tree resolves through, instead of materialising a second type system per + // module (and disagreeing with the tree about settings-dependent entity shapes). + request.DecompilerSettings = AppEnv.AppComposition.TryGetExport() + ?.CreateEffectiveDecompilerSettings() ?? new DecompilerSettings(); return request; } diff --git a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs index 5b7f99bad..a3c24c6b0 100644 --- a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs @@ -91,7 +91,7 @@ namespace ICSharpCode.ILSpy.TreeNodes var parentModule = parentAssembly.LoadedAssembly.GetMetadataFileOrNull(); if (parentModule != null) { - var parentTypeSystem = (MetadataModule?)parentModule.GetTypeSystemOrNull()?.MainModule; + var parentTypeSystem = (MetadataModule?)parentModule.GetTypeSystemWithCurrentOptionsOrNull()?.MainModule; if (parentTypeSystem != null) Children.Add(new AssemblyReferenceReferencedTypesTreeNode(parentTypeSystem, reference)); } diff --git a/ILSpy/TreeNodes/ExtensionTreeNode.cs b/ILSpy/TreeNodes/ExtensionTreeNode.cs index 1d9449c2c..599afe310 100644 --- a/ILSpy/TreeNodes/ExtensionTreeNode.cs +++ b/ILSpy/TreeNodes/ExtensionTreeNode.cs @@ -69,10 +69,11 @@ namespace ICSharpCode.ILSpy.TreeNodes | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations); - // Avalonia uses the constructor-time marker reference directly. WPF re-resolves - // through GetTypeSystemWithCurrentOptionsOrNull so language-version flips refresh - // the display string immediately; the Avalonia port doesn't yet expose that helper - // (see `extension-methods-tree` follow-ups in the tracker). + // Uses the constructor-time marker reference directly. The marker comes from the + // parent TypeTreeNode, which resolves through GetTypeSystemWithCurrentOptionsOrNull, + // so the chain is consistent with the current settings; per-access re-resolution (so + // language-version flips refresh the display string without rebuilding the node, as + // WPF does) remains a follow-up (see `extension-methods-tree` in the tracker). ITypeDefinition GetTypeDefinition() => MarkerMethod.DeclaringTypeDefinition!; protected override void LoadChildren() diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index 4cdb51fbe..67b06a763 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -94,7 +94,9 @@ namespace ICSharpCode.ILSpy.TreeNodes public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) { - var typeSystem = module.GetTypeSystemOrNull(); + // Enumerate via the type system matching this run's settings so the namespace + // listing agrees with what the per-type decompilation below produces. + var typeSystem = module.GetTypeSystemWithDecompilerSettingsOrNull(options.DecompilerSettings); if (typeSystem == null) { language.WriteCommentLine(output, "(type system unavailable)"); diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index ae3e3fcfa..ac7a1a687 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -137,7 +137,7 @@ namespace ICSharpCode.ILSpy.TreeNodes ITypeDefinition? ResolveTypeDefinition() { - var typeSystem = module.GetTypeSystemOrNull(); + var typeSystem = module.GetTypeSystemWithCurrentOptionsOrNull(); if (typeSystem == null) return null; return ((MetadataModule)typeSystem.MainModule).GetDefinition(handle);