From fd218c03ee09824097b58f6ca67199ee9340ac4c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 17 May 2026 23:30:33 +0200 Subject: [PATCH] Toolbar language-version dropdown affects decompile output Two halves to the fix: Assisted-by: Claude:claude-opus-4-7:Claude Code --- ...uageVersionAffectsDecompilerOutputTests.cs | 92 +++++++++++++++++++ ILSpy/Docking/DockWorkspace.cs | 6 +- ILSpy/TextView/DecompilerTabPageModel.cs | 25 ++++- 3 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 ILSpy.Tests/Languages/LanguageVersionAffectsDecompilerOutputTests.cs diff --git a/ILSpy.Tests/Languages/LanguageVersionAffectsDecompilerOutputTests.cs b/ILSpy.Tests/Languages/LanguageVersionAffectsDecompilerOutputTests.cs new file mode 100644 index 000000000..eabef6cf1 --- /dev/null +++ b/ILSpy.Tests/Languages/LanguageVersionAffectsDecompilerOutputTests.cs @@ -0,0 +1,92 @@ +// 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.Linq; +using System.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ILSpy.AppEnv; +using ILSpy.Languages; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +[TestFixture] +public class LanguageVersionAffectsDecompilerOutputTests +{ + [AvaloniaTest] + public async Task Changing_Language_Version_Triggers_Redecompile_And_Output_Reflects_New_Version() + { + // Picking System.Linq.Enumerable.Select (the IEnumerable+Func overload) — its body uses + // `is`-pattern-matching with declaration (C# 7+), so dropping to C# 6 forces the decompiler + // to fall back to explicit cast + null-check + variable assignment. Output text therefore + // differs sharply between C# 14 and C# 6. + // + // Without the fix the test fails two ways at once: + // 1. Setting LanguageService.CurrentVersion never triggers a re-decompile (the + // DockWorkspace listener watches CurrentLanguage only) so the wait for "text differs" + // times out. + // 2. Even if a re-decompile fired, TryGetLiveDecompilerSettings never reads + // CurrentVersion, so the produced DecompilerSettings still targets Latest and the + // output text would not change. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.IsExpanded = true; + // Two `Select` overloads (with and without index); the IEnumerable + Func + // one is the version-sensitive target. + var selectNode = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "Select" + && m.MethodDefinition.Parameters.Count == 2); + + var languageService = AppComposition.Current.GetExport(); + // Baseline: latest C# version so `is`-pattern-matching is used. + languageService.CurrentVersion = languageService.CurrentLanguage.LanguageVersions.Last(); + + vm.AssemblyTreeModel.SelectNode(selectNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + var latestText = tab.Text; + + // Act — drop to C# 6 (no `is`-pattern-matching declarations) and wait for the next + // decompile to land. + var cs6 = languageService.CurrentLanguage.LanguageVersions + .First(v => v.DisplayName.StartsWith("C# 6.0")); + languageService.CurrentVersion = cs6; + + await Waiters.WaitForAsync( + () => !tab.IsDecompiling && tab.Text != latestText, + TimeSpan.FromSeconds(15), + "re-decompile to fire after CurrentVersion change AND emit different output"); + + tab.Text.Should().NotBe(latestText, + "switching the language version must visibly change the decompiled output"); + } +} diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index a951a7000..3042e6351 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -461,12 +461,14 @@ namespace ILSpy.Docking void OnLanguagePropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(LanguageService.CurrentLanguage)) + if (e.PropertyName is nameof(LanguageService.CurrentLanguage) or nameof(LanguageService.CurrentVersion)) { if (ActiveDecompilerTab is { } tab) { tab.Language = languageService.CurrentLanguage; - // Re-decompile by re-assigning the same node so the tab refreshes for the new language. + // Re-decompile by re-assigning the same node so the tab refreshes for the new + // language or language version (the version is read inside + // TryGetLiveDecompilerSettings, which builds the per-run DecompilerSettings). var node = tab.CurrentNode; tab.CurrentNode = null; tab.CurrentNode = node; diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index d86727d8e..c75345041 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -578,14 +578,31 @@ namespace ILSpy.TextView Title = string.IsNullOrEmpty(output.Title) ? "(report)" : output.Title; } - // Pulls the live DecompilerSettings via MEF and returns a clone for this run. MEF - // resolve is wrapped in try/catch so design-time / minimal test hosts that bypass - // composition fall back to default settings rather than throwing. + // Pulls the live DecompilerSettings via MEF and returns a clone for this run. Also + // bakes the active LanguageService.CurrentVersion into the clone — without this the + // toolbar's Language-Version dropdown writes-through to LanguageSettings but never + // reaches the decompiler. MEF resolves are wrapped in try/catch so design-time / + // minimal test hosts that bypass composition fall back to default settings rather + // than throwing. static ICSharpCode.Decompiler.DecompilerSettings? TryGetLiveDecompilerSettings() { try { - return AppEnv.AppComposition.Current.GetExport().DecompilerSettings.Clone(); + var settings = AppEnv.AppComposition.Current.GetExport().DecompilerSettings.Clone(); + try + { + var version = AppEnv.AppComposition.Current.GetExport().CurrentVersion; + if (Enum.TryParse(version?.Version, out var languageVersion)) + settings.SetLanguageVersion(languageVersion); + else + settings.SetLanguageVersion(ICSharpCode.Decompiler.CSharp.LanguageVersion.Latest); + } + catch + { + // No LanguageService available (non-C# language or minimal host) — leave the + // settings at whatever default the source DecompilerSettings carried. + } + return settings; } catch {