Browse Source

Honor the user's decompiler settings in all decompilation paths

The Avalonia port gave DecompilationOptions a parameterless constructor
that silently defaults to new DecompilerSettings(). Several paths picked
it up and decompiled with default settings where the WPF version used
the user's current options: tree member filtering (CSharpLanguage.
ShowMember), PDB generation, the single-file / project / solution Save
Code paths, and the DEBUG decompile-all commands.

Promote the live-snapshot logic that was private to DecompilerTabPage-
Model (settings clone + Display-option bridge + toolbar language
version) to SettingsService.CreateEffectiveDecompilerSettings and use
it at every entry point. Remove the parameterless DecompilationOptions
constructor and make SolutionWriter require settings, so reaching for
defaults is an explicit choice rather than a silent fallback - that
default is exactly what masked these regressions. Search deliberately
keeps default settings (it only needs a type system to materialise).

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3404/head
Siegfried Pammer 4 weeks ago
parent
commit
b5a605e7a8
  1. 2
      ILSpy.Tests.Windows/ImageList/ImageListResourceEntryNodeTests.cs
  2. 2
      ILSpy.Tests/AssemblyList/AssemblyListDecompileTests.cs
  3. 3
      ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs
  4. 2
      ILSpy.Tests/Languages/ProjectExportTests.cs
  5. 100
      ILSpy.Tests/Languages/ShowMemberSettingsTests.cs
  6. 6
      ILSpy.Tests/Languages/SolutionExportTests.cs
  7. 2
      ILSpy.Tests/Resources/BamlResourceTests.cs
  8. 2
      ILSpy.Tests/Resources/ResourceFactoryTests.cs
  9. 8
      ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs
  10. 12
      ILSpy/Commands/DecompileAllCommand.cs
  11. 7
      ILSpy/Commands/PdbGenerator.cs
  12. 4
      ILSpy/Commands/SaveCodeHelper.cs
  13. 7
      ILSpy/Commands/SolutionExport.cs
  14. 6
      ILSpy/DecompilationOptions.cs
  15. 7
      ILSpy/Languages/CSharpLanguage.cs
  16. 2
      ILSpy/Options/DisplaySettingReactions.cs
  17. 47
      ILSpy/SettingsService.cs
  18. 14
      ILSpy/SolutionWriter.cs
  19. 61
      ILSpy/TextView/DecompilerTabPageModel.cs
  20. 4
      ILSpy/TreeNodes/AssemblyTreeNode.cs

2
ILSpy.Tests.Windows/ImageList/ImageListResourceEntryNodeTests.cs

@ -127,7 +127,7 @@ public class ImageListResourceEntryNodeTests
var output = new AvaloniaEditTextOutput(); var output = new AvaloniaEditTextOutput();
var language = AppComposition.Current.GetExport<LanguageService>().CurrentLanguage; var language = AppComposition.Current.GetExport<LanguageService>().CurrentLanguage;
node.Decompile(language, output, new DecompilationOptions()); node.Decompile(language, output, new DecompilationOptions(new DecompilerSettings()));
output.UIElements.Should().HaveCount(1, output.UIElements.Should().HaveCount(1,
"the parent's Decompile contributes exactly one inline preview panel"); "the parent's Decompile contributes exactly one inline preview panel");

2
ILSpy.Tests/AssemblyList/AssemblyListDecompileTests.cs

@ -53,7 +53,7 @@ public class AssemblyListDecompileTests
var output = new PlainTextOutput(); var output = new PlainTextOutput();
// Decompiling every assembly is far too slow for a test, so cancel up front: the header // Decompiling every assembly is far too slow for a test, so cancel up front: the header
// and the first rule are written before the first member decompilation observes the token. // and the first rule are written before the first member decompilation observes the token.
var options = new DecompilationOptions { CancellationToken = new CancellationToken(true) }; var options = new DecompilationOptions(new DecompilerSettings()) { CancellationToken = new CancellationToken(true) };
try try
{ {

3
ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs

@ -23,6 +23,7 @@ using Avalonia.Headless.NUnit;
using AwesomeAssertions; using AwesomeAssertions;
using ICSharpCode.Decompiler;
using ICSharpCode.ILSpy; using ICSharpCode.ILSpy;
using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.Languages;
@ -60,7 +61,7 @@ public class CSharpILMixedLanguageTests
string Decompile(Language language) string Decompile(Language language)
{ {
var output = new AvaloniaEditTextOutput(); var output = new AvaloniaEditTextOutput();
language.DecompileMethod(method, output, new DecompilationOptions()); language.DecompileMethod(method, output, new DecompilationOptions(new DecompilerSettings()));
return output.GetText(); return output.GetText();
} }

2
ILSpy.Tests/Languages/ProjectExportTests.cs

@ -79,7 +79,7 @@ public class ProjectExportTests
Directory.CreateDirectory(tempDir); Directory.CreateDirectory(tempDir);
try try
{ {
var options = new ICSharpCode.ILSpy.DecompilationOptions { var options = new ICSharpCode.ILSpy.DecompilationOptions(new DecompilerSettings()) {
FullDecompilation = true, FullDecompilation = true,
SaveAsProjectDirectory = tempDir, SaveAsProjectDirectory = tempDir,
}; };

100
ILSpy.Tests/Languages/ShowMemberSettingsTests.cs

@ -0,0 +1,100 @@
// 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;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Languages;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Languages;
// CSharpLanguage.ShowMember drives which members the assembly tree shows at all: every member
// tree node consults it (unless the API-visibility filter is set to "All"). It must evaluate
// CSharpDecompiler.MemberIsHidden against the LIVE decompiler settings, not a default-constructed
// DecompilerSettings — otherwise toggling a decompiler option that unhides compiler-generated
// members (here: ArrayInitializers, which hides "<PrivateImplementationDetails>") has no effect
// on the tree.
[TestFixture]
public class ShowMemberSettingsTests
{
/// <summary>
/// Emits an assembly containing a top-level [CompilerGenerated] type named
/// "&lt;PrivateImplementationDetails&gt;" and returns its type definition.
/// MemberIsHidden hides that type iff <c>DecompilerSettings.ArrayInitializers</c> is set,
/// which makes it a minimal settings-sensitive probe for ShowMember.
/// </summary>
static ITypeDefinition EmitPrivateImplementationDetails()
{
const string name = "ShowMemberFixture";
var ab = new PersistedAssemblyBuilder(new AssemblyName(name), typeof(object).Assembly);
var module = ab.DefineDynamicModule(name);
var details = module.DefineType("<PrivateImplementationDetails>",
TypeAttributes.NotPublic | TypeAttributes.Class | TypeAttributes.Sealed);
details.SetCustomAttribute(new CustomAttributeBuilder(
typeof(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes)!, []));
details.CreateType();
var dir = Path.Combine(Path.GetTempPath(), $"ILSpyShowMember_{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, $"{name}.dll");
ab.Save(path);
var file = new PEFile(path);
var resolver = new UniversalAssemblyResolver(path, throwOnError: false,
targetFramework: file.DetectTargetFrameworkId());
var typeSystem = new DecompilerTypeSystem(file, resolver);
return typeSystem.MainModule.TypeDefinitions
.Single(t => t.Name == "<PrivateImplementationDetails>");
}
[AvaloniaTest]
public void ShowMember_hides_PrivateImplementationDetails_under_default_settings()
{
var language = AppComposition.Current.GetExport<LanguageService>().GetLanguage("C#");
language.ShowMember(EmitPrivateImplementationDetails()).Should().BeFalse(
"ArrayInitializers defaults to true, which hides <PrivateImplementationDetails>");
}
[AvaloniaTest]
public void ShowMember_honours_the_live_decompiler_settings()
{
var settingsService = AppComposition.Current.GetExport<SettingsService>();
settingsService.DecompilerSettings.ArrayInitializers = false;
var language = AppComposition.Current.GetExport<LanguageService>().GetLanguage("C#");
language.ShowMember(EmitPrivateImplementationDetails()).Should().BeTrue(
"with ArrayInitializers off, MemberIsHidden no longer hides <PrivateImplementationDetails>, " +
"so the tree must show it");
}
}

6
ILSpy.Tests/Languages/SolutionExportTests.cs

@ -18,12 +18,14 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Headless.NUnit; using Avalonia.Headless.NUnit;
using AwesomeAssertions; using AwesomeAssertions;
using ICSharpCode.Decompiler;
using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.Languages;
@ -60,7 +62,7 @@ public class SolutionExportTests
var slnPath = Path.Combine(tempDir, "Solution.sln"); var slnPath = Path.Combine(tempDir, "Solution.sln");
try try
{ {
var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync(slnPath, language, assemblies); var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync(slnPath, language, assemblies, CancellationToken.None, new DecompilerSettings());
result.Success.Should().BeTrue( result.Success.Should().BeTrue(
"the solution export should succeed for valid assemblies. Status:\n" + result.StatusText); "the solution export should succeed for valid assemblies. Status:\n" + result.StatusText);
@ -106,7 +108,7 @@ public class SolutionExportTests
// The same assembly twice collides on ShortName: the writer must refuse rather than // The same assembly twice collides on ShortName: the writer must refuse rather than
// clobber one project directory with another. // clobber one project directory with another.
var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync( var result = await ICSharpCode.ILSpy.SolutionWriter.CreateSolutionAsync(
slnPath, language, new[] { assembly, assembly }); slnPath, language, new[] { assembly, assembly }, CancellationToken.None, new DecompilerSettings());
result.Success.Should().BeFalse("duplicate assembly names cannot produce a valid solution"); result.Success.Should().BeFalse("duplicate assembly names cannot produce a valid solution");
result.StatusText.Should().Contain("Duplicate assembly names"); result.StatusText.Should().Contain("Duplicate assembly names");

2
ILSpy.Tests/Resources/BamlResourceTests.cs

@ -94,7 +94,7 @@ public class BamlResourceTests
// (the handler doesn't touch the options for CanHandle). // (the handler doesn't touch the options for CanHandle).
EnsureComposition(); EnsureComposition();
var handler = new BamlResourceFileHandler(); var handler = new BamlResourceFileHandler();
var context = new ResourceFileHandlerContext(new DecompilationOptions()); var context = new ResourceFileHandlerContext(new DecompilationOptions(new DecompilerSettings()));
// Act + Assert — three positive variants and two rejections. // Act + Assert — three positive variants and two rejections.
handler.CanHandle("MainWindow.baml", context).Should().BeTrue(); handler.CanHandle("MainWindow.baml", context).Should().BeTrue();

2
ILSpy.Tests/Resources/ResourceFactoryTests.cs

@ -125,7 +125,7 @@ public class ResourceFactoryTests
var language = AppComposition.Current.GetExport<LanguageService>().CurrentLanguage; var language = AppComposition.Current.GetExport<LanguageService>().CurrentLanguage;
// Act — decompile the resource node into the AvaloniaEdit text output. // Act — decompile the resource node into the AvaloniaEdit text output.
node.Decompile(language, output, new DecompilationOptions()); node.Decompile(language, output, new DecompilationOptions(new DecompilerSettings()));
// Assert — exactly three UI elements (string grid + object grid + inherited Save button) // Assert — exactly three UI elements (string grid + object grid + inherited Save button)
// and they all materialise as Avalonia Controls. // and they all materialise as Avalonia Controls.

8
ILSpy.Tests/TextView/DisplaySettingsBridgeTests.cs

@ -42,7 +42,7 @@ public class DisplaySettingsBridgeTests
var display = new DisplaySettings { FoldBraces = true, ShowDebugInfo = true }; var display = new DisplaySettings { FoldBraces = true, ShowDebugInfo = true };
var settings = new DecompilerSettings { FoldBraces = false, ShowDebugInfo = false }; var settings = new DecompilerSettings { FoldBraces = false, ShowDebugInfo = false };
DecompilerTabPageModel.ApplyDisplaySettings(settings, display); SettingsService.ApplyDisplaySettings(settings, display);
settings.FoldBraces.Should().BeTrue(); settings.FoldBraces.Should().BeTrue();
settings.ShowDebugInfo.Should().BeTrue(); settings.ShowDebugInfo.Should().BeTrue();
@ -54,7 +54,7 @@ public class DisplaySettingsBridgeTests
var display = new DisplaySettings { IndentationUseTabs = false, IndentationSize = 2 }; var display = new DisplaySettings { IndentationUseTabs = false, IndentationSize = 2 };
var settings = new DecompilerSettings(); var settings = new DecompilerSettings();
DecompilerTabPageModel.ApplyDisplaySettings(settings, display); SettingsService.ApplyDisplaySettings(settings, display);
settings.CSharpFormattingOptions.IndentationString.Should().Be(" ", "two spaces"); settings.CSharpFormattingOptions.IndentationString.Should().Be(" ", "two spaces");
} }
@ -65,7 +65,7 @@ public class DisplaySettingsBridgeTests
var display = new DisplaySettings { IndentationUseTabs = true, IndentationSize = 4, IndentationTabSize = 4 }; var display = new DisplaySettings { IndentationUseTabs = true, IndentationSize = 4, IndentationTabSize = 4 };
var settings = new DecompilerSettings(); var settings = new DecompilerSettings();
DecompilerTabPageModel.ApplyDisplaySettings(settings, display); SettingsService.ApplyDisplaySettings(settings, display);
settings.CSharpFormattingOptions.IndentationString.Should().Be("\t", "one tab"); settings.CSharpFormattingOptions.IndentationString.Should().Be("\t", "one tab");
} }
@ -76,7 +76,7 @@ public class DisplaySettingsBridgeTests
var display = new DisplaySettings { ExpandUsingDeclarations = true, ExpandMemberDefinitions = true }; var display = new DisplaySettings { ExpandUsingDeclarations = true, ExpandMemberDefinitions = true };
var settings = new DecompilerSettings { ExpandUsingDeclarations = false, ExpandMemberDefinitions = false }; var settings = new DecompilerSettings { ExpandUsingDeclarations = false, ExpandMemberDefinitions = false };
DecompilerTabPageModel.ApplyDisplaySettings(settings, display); SettingsService.ApplyDisplaySettings(settings, display);
settings.ExpandUsingDeclarations.Should().BeTrue(); settings.ExpandUsingDeclarations.Should().BeTrue();
settings.ExpandMemberDefinitions.Should().BeTrue(); settings.ExpandMemberDefinitions.Should().BeTrue();

12
ILSpy/Commands/DecompileAllCommand.cs

@ -66,6 +66,8 @@ namespace ICSharpCode.ILSpy.Commands
async Task ExecuteAsync() async Task ExecuteAsync()
{ {
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new ICSharpCode.Decompiler.DecompilerSettings();
// Run in a dedicated frozen tab so navigation cannot cancel this long run. // Run in a dedicated frozen tab so navigation cannot cancel this long run.
await dockWorkspace.RunInNewTabAsync("Decompiling all assemblies…", token => Task.Run(() => { await dockWorkspace.RunInNewTabAsync("Decompiling all assemblies…", token => Task.Run(() => {
var output = new AvaloniaEditTextOutput { Title = "Decompile All" }; var output = new AvaloniaEditTextOutput { Title = "Decompile All" };
@ -82,7 +84,7 @@ namespace ICSharpCode.ILSpy.Commands
try try
{ {
using var writer = new StreamWriter(path); using var writer = new StreamWriter(path);
var options = new DecompilationOptions { var options = new DecompilationOptions(settings) {
CancellationToken = token, CancellationToken = token,
FullDecompilation = true, FullDecompilation = true,
}; };
@ -133,6 +135,8 @@ namespace ICSharpCode.ILSpy.Commands
async Task ExecuteAsync() async Task ExecuteAsync()
{ {
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new ICSharpCode.Decompiler.DecompilerSettings();
// Run in a dedicated frozen tab so navigation cannot cancel this long run. // Run in a dedicated frozen tab so navigation cannot cancel this long run.
await dockWorkspace.RunInNewTabAsync("Disassembling all assemblies…", token => Task.Run(() => { await dockWorkspace.RunInNewTabAsync("Disassembling all assemblies…", token => Task.Run(() => {
var output = new AvaloniaEditTextOutput { Title = "Disassemble All" }; var output = new AvaloniaEditTextOutput { Title = "Disassemble All" };
@ -150,7 +154,7 @@ namespace ICSharpCode.ILSpy.Commands
try try
{ {
using var writer = new StreamWriter(path); using var writer = new StreamWriter(path);
var options = new DecompilationOptions { var options = new DecompilationOptions(settings) {
CancellationToken = token, CancellationToken = token,
FullDecompilation = true, FullDecompilation = true,
}; };
@ -204,10 +208,12 @@ namespace ICSharpCode.ILSpy.Commands
var nodes = assemblyTreeModel.SelectedItems.OfType<ILSpyTreeNode>().ToArray(); var nodes = assemblyTreeModel.SelectedItems.OfType<ILSpyTreeNode>().ToArray();
if (nodes.Length == 0) if (nodes.Length == 0)
return; return;
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new ICSharpCode.Decompiler.DecompilerSettings();
// Run in a dedicated frozen tab so navigation cannot cancel this long run. // Run in a dedicated frozen tab so navigation cannot cancel this long run.
await dockWorkspace.RunInNewTabAsync("Decompiling 100×…", token => Task.Run(() => { await dockWorkspace.RunInNewTabAsync("Decompiling 100×…", token => Task.Run(() => {
var watch = Stopwatch.StartNew(); var watch = Stopwatch.StartNew();
var options = new DecompilationOptions { CancellationToken = token }; var options = new DecompilationOptions(settings) { CancellationToken = token };
for (int i = 0; i < NumRuns; i++) for (int i = 0; i < NumRuns; i++)
{ {
foreach (var node in nodes) foreach (var node in nodes)

7
ILSpy/Commands/PdbGenerator.cs

@ -32,6 +32,7 @@ using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpyX; using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView; using ICSharpCode.ILSpyX.TreeView;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Docking;
using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.TreeNodes;
@ -106,6 +107,11 @@ namespace ICSharpCode.ILSpy.Commands
? string.Format(Resources.GeneratingPortablePDB, supported.Keys.First().ShortName) ? string.Format(Resources.GeneratingPortablePDB, supported.Keys.First().ShortName)
: string.Format(Resources.GeneratingPortablePDB, supported.Count + " assemblies"); : string.Format(Resources.GeneratingPortablePDB, supported.Count + " assemblies");
// The PDB embeds decompiled sources, so honor the user's current decompiler settings
// (snapshot once; the generation runs off the UI thread).
var settings = AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new DecompilerSettings();
// Run in a dedicated frozen tab so browsing the tree while PDBs generate can't cancel it. // Run in a dedicated frozen tab so browsing the tree while PDBs generate can't cancel it.
await dockWorkspace.RunInNewTabAsync(title, token => Task.Run(() => { await dockWorkspace.RunInNewTabAsync(title, token => Task.Run(() => {
var output = new AvaloniaEditTextOutput { Title = "Generate Portable PDB" }; var output = new AvaloniaEditTextOutput { Title = "Generate Portable PDB" };
@ -117,7 +123,6 @@ namespace ICSharpCode.ILSpy.Commands
{ {
using var stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write); using var stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write);
var resolver = assembly.GetAssemblyResolver(); var resolver = assembly.GetAssemblyResolver();
var settings = new DecompilerSettings();
var decompiler = new CSharpDecompiler(file, resolver, settings) { var decompiler = new CSharpDecompiler(file, resolver, settings) {
CancellationToken = token, CancellationToken = token,
}; };

4
ILSpy/Commands/SaveCodeHelper.cs

@ -107,7 +107,9 @@ namespace ICSharpCode.ILSpy.Commands
ArgumentNullException.ThrowIfNull(node); ArgumentNullException.ThrowIfNull(node);
ArgumentNullException.ThrowIfNull(language); ArgumentNullException.ThrowIfNull(language);
var options = new DecompilationOptions { var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new ICSharpCode.Decompiler.DecompilerSettings();
var options = new DecompilationOptions(settings) {
FullDecompilation = true, FullDecompilation = true,
EscapeInvalidIdentifiers = true, EscapeInvalidIdentifiers = true,
CancellationToken = ct, CancellationToken = ct,

7
ILSpy/Commands/SolutionExport.cs

@ -73,10 +73,15 @@ namespace ICSharpCode.ILSpy.Commands
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
return; return;
// Snapshot the user's current decompiler settings for the whole export, like the
// per-project export path does.
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new ICSharpCode.Decompiler.DecompilerSettings();
// Run in a dedicated frozen tab so browsing the tree while the export runs can't cancel it. // Run in a dedicated frozen tab so browsing the tree while the export runs can't cancel it.
await dockWorkspace.RunInNewTabAsync("Exporting solution", async (token, progress) => { await dockWorkspace.RunInNewTabAsync("Exporting solution", async (token, progress) => {
var result = await SolutionWriter.CreateSolutionAsync(path, language, assemblies, token, var result = await SolutionWriter.CreateSolutionAsync(path, language, assemblies, token,
settings: null, strongNameKeyFile: null, progress: progress) settings, strongNameKeyFile: null, progress: progress)
.ConfigureAwait(false); .ConfigureAwait(false);
var o = new AvaloniaEditTextOutput { Title = Resources._SaveCode }; var o = new AvaloniaEditTextOutput { Title = Resources._SaveCode };
o.Write(result.StatusText); o.Write(result.StatusText);

6
ILSpy/DecompilationOptions.cs

@ -75,11 +75,13 @@ namespace ICSharpCode.ILSpy
/// </summary> /// </summary>
public IProgress<DecompilationProgress>? ProgressIndicator { get; set; } public IProgress<DecompilationProgress>? ProgressIndicator { get; set; }
// Deliberately no parameterless constructor: every decompilation must make an explicit
// choice of settings. Callers inside the app want the user's current settings (see
// SettingsService.CreateEffectiveDecompilerSettings), and a silent new DecompilerSettings()
// default has repeatedly masked exactly that bug.
public DecompilationOptions(DecompilerSettings settings) public DecompilationOptions(DecompilerSettings settings)
{ {
DecompilerSettings = settings; DecompilerSettings = settings;
} }
public DecompilationOptions() : this(new DecompilerSettings()) { }
} }
} }

7
ILSpy/Languages/CSharpLanguage.cs

@ -257,7 +257,12 @@ namespace ICSharpCode.ILSpy.Languages
var assembly = member.ParentModule?.MetadataFile; var assembly = member.ParentModule?.MetadataFile;
if (assembly == null) if (assembly == null)
return true; return true;
return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, new DecompilerSettings()); // Use the effective settings, not defaults: which members MemberIsHidden hides
// depends on the decompiler options (and language version), and the tree must agree
// with what the text view actually elides.
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new DecompilerSettings();
return !CSharpDecompiler.MemberIsHidden(assembly, member.MetadataToken, settings);
} }
public override RichText GetRichTextTooltip(IEntity entity) public override RichText GetRichTextTooltip(IEntity entity)

2
ILSpy/Options/DisplaySettingReactions.cs

@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy.Options
[nameof(DisplaySettings.UseNestedNamespaceNodes)] = DisplaySettingReaction.TreeShape, [nameof(DisplaySettings.UseNestedNamespaceNodes)] = DisplaySettingReaction.TreeShape,
[nameof(DisplaySettings.HideEmptyMetadataTables)] = DisplaySettingReaction.TreeShape, [nameof(DisplaySettings.HideEmptyMetadataTables)] = DisplaySettingReaction.TreeShape,
// Decompiler / disassembler output (see DecompilerTabPageModel.ApplyDisplaySettings + // Decompiler / disassembler output (see SettingsService.ApplyDisplaySettings +
// GetIndentationString, and CSharpILMixedLanguage / ILLanguage for the IL-detail ones). // GetIndentationString, and CSharpILMixedLanguage / ILLanguage for the IL-detail ones).
[nameof(DisplaySettings.FoldBraces)] = DisplaySettingReaction.Redecompile, [nameof(DisplaySettings.FoldBraces)] = DisplaySettingReaction.Redecompile,
[nameof(DisplaySettings.ExpandMemberDefinitions)] = DisplaySettingReaction.Redecompile, [nameof(DisplaySettings.ExpandMemberDefinitions)] = DisplaySettingReaction.Redecompile,

47
ILSpy/SettingsService.cs

@ -16,6 +16,7 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System;
using System.ComponentModel; using System.ComponentModel;
using System.Composition; using System.Composition;
@ -57,6 +58,52 @@ namespace ICSharpCode.ILSpy
public MiscSettings MiscSettings => GetSettings<MiscSettings>(); public MiscSettings MiscSettings => GetSettings<MiscSettings>();
/// <summary>
/// Returns the effective decompiler settings a decompilation started right now would
/// use: a clone of <see cref="DecompilerSettings"/> with the Display options bridged in
/// and the language version selected in the toolbar applied. Mutating the returned
/// instance does not affect the persisted settings.
/// </summary>
public Decompiler.DecompilerSettings CreateEffectiveDecompilerSettings()
{
var settings = DecompilerSettings.Clone();
ApplyDisplaySettings(settings, DisplaySettings);
// No LanguageService (non-C# language or minimal test host) leaves the version
// null, so the language version falls back to Latest below.
var version = AppEnv.AppComposition.TryGetExport<Languages.LanguageService>()?.CurrentVersion;
if (Enum.TryParse<Decompiler.CSharp.LanguageVersion>(version?.Version, out var languageVersion))
settings.SetLanguageVersion(languageVersion);
else
settings.SetLanguageVersion(Decompiler.CSharp.LanguageVersion.Latest);
return settings;
}
/// <summary>
/// Bridges the Display options that affect decompiler output into <paramref name="settings"/>:
/// the fold-expansion flags (TextTokenWriter reads them to set each fold's DefaultClosed),
/// brace folding, debug-symbol info, and the indentation string. Without this these Display
/// options would have no effect on the produced source.
/// </summary>
internal static void ApplyDisplaySettings(Decompiler.DecompilerSettings settings, DisplaySettings display)
{
settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations;
settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions;
settings.FoldBraces = display.FoldBraces;
settings.ShowDebugInfo = display.ShowDebugInfo;
settings.CSharpFormattingOptions.IndentationString = GetIndentationString(display);
}
static string GetIndentationString(DisplaySettings display)
{
if (display.IndentationUseTabs)
{
int tabs = display.IndentationSize / display.IndentationTabSize;
int spaces = display.IndentationSize % display.IndentationTabSize;
return new string('\t', tabs) + new string(' ', spaces);
}
return new string(' ', display.IndentationSize);
}
public Updates.UpdateSettings UpdateSettings => GetSettings<Updates.UpdateSettings>(); public Updates.UpdateSettings UpdateSettings => GetSettings<Updates.UpdateSettings>();
AssemblyListManager? assemblyListManager; AssemblyListManager? assemblyListManager;

14
ILSpy/SolutionWriter.cs

@ -56,20 +56,20 @@ namespace ICSharpCode.ILSpy
/// or whitespace.</exception> /// or whitespace.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="language"/> or /// <exception cref="ArgumentNullException">Thrown when <paramref name="language"/> or
/// <paramref name="assemblies"/> is null.</exception> /// <paramref name="assemblies"/> is null.</exception>
/// <param name="settings">Decompiler settings each project is decompiled with. When null, /// <param name="settings">Decompiler settings each project is decompiled with.</param>
/// each project uses the language's own defaults (preserves the quick "Save Code" path).</param>
/// <param name="strongNameKeyFile">Optional <c>.snk</c> copied into every project and emitted /// <param name="strongNameKeyFile">Optional <c>.snk</c> copied into every project and emitted
/// as <c>&lt;AssemblyOriginatorKeyFile&gt;</c>.</param> /// as <c>&lt;AssemblyOriginatorKeyFile&gt;</c>.</param>
public static Task<SolutionExportResult> CreateSolutionAsync(string solutionFilePath, public static Task<SolutionExportResult> CreateSolutionAsync(string solutionFilePath,
Language language, IReadOnlyList<LoadedAssembly> assemblies, Language language, IReadOnlyList<LoadedAssembly> assemblies,
CancellationToken cancellationToken = default, CancellationToken cancellationToken,
DecompilerSettings? settings = null, string? strongNameKeyFile = null, DecompilerSettings settings, string? strongNameKeyFile = null,
IProgress<DecompilationProgress>? progress = null) IProgress<DecompilationProgress>? progress = null)
{ {
if (string.IsNullOrWhiteSpace(solutionFilePath)) if (string.IsNullOrWhiteSpace(solutionFilePath))
throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath)); throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath));
ArgumentNullException.ThrowIfNull(language); ArgumentNullException.ThrowIfNull(language);
ArgumentNullException.ThrowIfNull(assemblies); ArgumentNullException.ThrowIfNull(assemblies);
ArgumentNullException.ThrowIfNull(settings);
return new SolutionWriter(solutionFilePath, settings, strongNameKeyFile, progress) return new SolutionWriter(solutionFilePath, settings, strongNameKeyFile, progress)
.CreateSolutionAsync(assemblies, language, cancellationToken); .CreateSolutionAsync(assemblies, language, cancellationToken);
@ -77,14 +77,14 @@ namespace ICSharpCode.ILSpy
readonly string solutionFilePath; readonly string solutionFilePath;
readonly string solutionDirectory; readonly string solutionDirectory;
readonly DecompilerSettings? settings; readonly DecompilerSettings settings;
readonly string? strongNameKeyFile; readonly string? strongNameKeyFile;
readonly IProgress<DecompilationProgress>? progress; readonly IProgress<DecompilationProgress>? progress;
readonly ConcurrentBag<ProjectItem> projects; readonly ConcurrentBag<ProjectItem> projects;
readonly ConcurrentBag<string> statusOutput; readonly ConcurrentBag<string> statusOutput;
int completedAssemblies; int completedAssemblies;
SolutionWriter(string solutionFilePath, DecompilerSettings? settings, string? strongNameKeyFile, SolutionWriter(string solutionFilePath, DecompilerSettings settings, string? strongNameKeyFile,
IProgress<DecompilationProgress>? progress) IProgress<DecompilationProgress>? progress)
{ {
this.solutionFilePath = solutionFilePath; this.solutionFilePath = solutionFilePath;
@ -224,7 +224,7 @@ namespace ICSharpCode.ILSpy
try try
{ {
var options = settings != null ? new DecompilationOptions(settings) : new DecompilationOptions(); var options = new DecompilationOptions(settings);
options.FullDecompilation = true; options.FullDecompilation = true;
options.EscapeInvalidIdentifiers = true; options.EscapeInvalidIdentifiers = true;
options.CancellationToken = ct; options.CancellationToken = ct;

61
ILSpy/TextView/DecompilerTabPageModel.cs

@ -514,13 +514,10 @@ namespace ICSharpCode.ILSpy.TextView
{ {
(output, _) = await Task.Run(() => { (output, _) = await Task.Run(() => {
var output = new AvaloniaEditTextOutput { LengthLimit = outputLengthLimit }; var output = new AvaloniaEditTextOutput { LengthLimit = outputLengthLimit };
var options = decompilerSettings != null // decompilerSettings is null only in design-time / minimal test hosts
? new DecompilationOptions(decompilerSettings) { // without composition; fall back to defaults there.
CancellationToken = cts.Token, var options = new DecompilationOptions(
StepLimit = stepLimit, decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) {
IsDebug = isDebug,
}
: new DecompilationOptions {
CancellationToken = cts.Token, CancellationToken = cts.Token,
StepLimit = stepLimit, StepLimit = stepLimit,
IsDebug = isDebug, IsDebug = isDebug,
@ -714,52 +711,10 @@ namespace ICSharpCode.ILSpy.TextView
Text = text; Text = text;
} }
// Pulls the live DecompilerSettings via MEF and returns a clone for this run. Also // Pulls the effective DecompilerSettings (clone + Display options + toolbar language
// bakes the active LanguageService.CurrentVersion into the clone — without this the // version) for this run. Resolves via TryGetExport so design-time / minimal test hosts
// toolbar's Language-Version dropdown writes-through to LanguageSettings but never // that bypass composition fall back to default settings rather than throwing.
// reaches the decompiler. Resolves go through TryGetExport so design-time / minimal test
// hosts that bypass composition fall back to default settings rather than throwing.
static ICSharpCode.Decompiler.DecompilerSettings? TryGetLiveDecompilerSettings() static ICSharpCode.Decompiler.DecompilerSettings? TryGetLiveDecompilerSettings()
{ => AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings();
var settingsService = AppEnv.AppComposition.TryGetExport<SettingsService>();
if (settingsService is null)
return null;
var settings = settingsService.DecompilerSettings.Clone();
ApplyDisplaySettings(settings, settingsService.DisplaySettings);
// No LanguageService (non-C# language or minimal host) leaves version null, so the
// language version falls back to Latest below.
var version = AppEnv.AppComposition.TryGetExport<Languages.LanguageService>()?.CurrentVersion;
if (Enum.TryParse<ICSharpCode.Decompiler.CSharp.LanguageVersion>(version?.Version, out var languageVersion))
settings.SetLanguageVersion(languageVersion);
else
settings.SetLanguageVersion(ICSharpCode.Decompiler.CSharp.LanguageVersion.Latest);
return settings;
}
/// <summary>
/// Bridges the Display options that affect decompiler output into <paramref name="settings"/>:
/// the fold-expansion flags (TextTokenWriter reads them to set each fold's DefaultClosed),
/// brace folding, debug-symbol info, and the indentation string. Without this these Display
/// options would have no effect on the produced source.
/// </summary>
internal static void ApplyDisplaySettings(ICSharpCode.Decompiler.DecompilerSettings settings, DisplaySettings display)
{
settings.ExpandUsingDeclarations = display.ExpandUsingDeclarations;
settings.ExpandMemberDefinitions = display.ExpandMemberDefinitions;
settings.FoldBraces = display.FoldBraces;
settings.ShowDebugInfo = display.ShowDebugInfo;
settings.CSharpFormattingOptions.IndentationString = GetIndentationString(display);
}
static string GetIndentationString(DisplaySettings display)
{
if (display.IndentationUseTabs)
{
int tabs = display.IndentationSize / display.IndentationTabSize;
int spaces = display.IndentationSize % display.IndentationTabSize;
return new string('\t', tabs) + new string(' ', spaces);
}
return new string(' ', display.IndentationSize);
}
} }
} }

4
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -194,8 +194,10 @@ namespace ICSharpCode.ILSpy.TreeNodes
var ext = Path.GetExtension(path); var ext = Path.GetExtension(path);
var isProject = string.Equals(ext, language.ProjectFileExtension, StringComparison.OrdinalIgnoreCase); var isProject = string.Equals(ext, language.ProjectFileExtension, StringComparison.OrdinalIgnoreCase);
var settings = AppEnv.AppComposition.TryGetExport<SettingsService>()?.CreateEffectiveDecompilerSettings()
?? new ICSharpCode.Decompiler.DecompilerSettings();
await Task.Run(() => { await Task.Run(() => {
var options = new DecompilationOptions { var options = new DecompilationOptions(settings) {
FullDecompilation = true, FullDecompilation = true,
EscapeInvalidIdentifiers = true, EscapeInvalidIdentifiers = true,
}; };

Loading…
Cancel
Save