Browse Source

Share the settings-derived type system between tree, search, and views

LoadedAssembly caches one type system per TypeSystemOptions value, but
the consumers disagreed on which one to use: tree nodes and the
metadata navigator resolved through GetTypeSystemOrNull (a separate
default-options instance) and the search built its request with default
settings. Each module could therefore materialise several type systems,
and none of the display surfaces respected settings that change entity
shapes (nullability, tuples, extension methods, ...). WPF routed all of
these through GetTypeSystemWithCurrentOptionsOrNull.

Reintroduce that helper on top of CreateEffectiveDecompilerSettings and
use it in TypeTreeNode, AssemblyReferenceTreeNode, and the metadata
navigator; the search request now derives its settings the same way, so
all of them hit the same cached instance. NamespaceTreeNode.Decompile
enumerates through the type system matching the decompilation options
it was handed.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3404/head
Siegfried Pammer 4 weeks ago
parent
commit
ba7fe1d75f
  1. 91
      ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs
  2. 44
      ILSpy/ExtensionMethods.cs
  3. 2
      ILSpy/Metadata/MetadataNavigator.cs
  4. 15
      ILSpy/Search/RunningSearch.cs
  5. 2
      ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs
  6. 9
      ILSpy/TreeNodes/ExtensionTreeNode.cs
  7. 4
      ILSpy/TreeNodes/NamespaceTreeNode.cs
  8. 2
      ILSpy/TreeNodes/TypeTreeNode.cs

91
ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs

@ -0,0 +1,91 @@ @@ -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<SettingsService>();
var fixture = await vm.OpenFixtureAsync();
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
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<SettingsService>();
// 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<LanguageService>().CurrentLanguage;
var search = new RunningSearch(
new[] { fixture }, FixtureAssembly.TypeName, SearchMode.TypeAndMember, language,
ApiVisibility.PublicAndInternal, new AvaloniaSearchResultFactory(language),
new ObservableCollection<SearchResult>(), 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");
}
}

44
ILSpy/ExtensionMethods.cs

@ -0,0 +1,44 @@ @@ -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
{
/// <summary>
/// Returns the cached type system for <paramref name="file"/> derived from the user's
/// current decompiler settings (see
/// <see cref="SettingsService.CreateEffectiveDecompilerSettings"/>). 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).
/// </summary>
public static ICompilation? GetTypeSystemWithCurrentOptionsOrNull(this MetadataFile file)
{
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new Decompiler.DecompilerSettings();
return file.GetTypeSystemWithDecompilerSettingsOrNull(settings);
}
}
}

2
ILSpy/Metadata/MetadataNavigator.cs

@ -75,7 +75,7 @@ namespace ICSharpCode.ILSpy.Metadata @@ -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

15
ILSpy/Search/RunningSearch.cs

@ -254,15 +254,18 @@ namespace ICSharpCode.ILSpy.Search @@ -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<SettingsService>()
?.CreateEffectiveDecompilerSettings() ?? new DecompilerSettings();
return request;
}

2
ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs

@ -91,7 +91,7 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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));
}

9
ILSpy/TreeNodes/ExtensionTreeNode.cs

@ -69,10 +69,11 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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()

4
ILSpy/TreeNodes/NamespaceTreeNode.cs

@ -94,7 +94,9 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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)");

2
ILSpy/TreeNodes/TypeTreeNode.cs

@ -137,7 +137,7 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -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);

Loading…
Cancel
Save