mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
ILSpy resolves an assembly's references against the target framework it detects from the TargetFrameworkAttribute. When that attribute is missing, wrong, or the user wants to force a different framework, there was no way to hint the correct one, so references could resolve against the wrong runtime pack or framework directory. A LoadedAssembly can now carry a TargetFrameworkIdOverride that short-circuits detection (it is the single value every LoadedAssembly-based resolution path reads), is persisted in the assembly-list XML, and is carried across a reload so a runtime change re-resolves against the new framework. The "Set Target Framework" context-menu entry edits it through a dialog with a free-form text box and an always-visible list of common monikers to pick from (the app forces overlay popups, so a dropdown would be clamped inside the small dialog); input is validated and converted from the short TFM users know (net48) to the long FrameworkName form the resolver consumes (.NETFramework,Version=v4.8) via NuGet. The direct DetectTargetFrameworkId callers in the decompiler core (project export, language version) intentionally keep reading the real attribute; only reference resolution is overridden. Resurrects a 2020 prototype (branch tfmoverride) re-implemented against the current ILSpyX/Avalonia code, whose surrounding structures no longer matched. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3839/head
22 changed files with 791 additions and 45 deletions
@ -0,0 +1,148 @@ |
|||||||
|
// 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 ICSharpCode.ILSpyX; |
||||||
|
|
||||||
|
using ICSharpCode.ILSpy.AppEnv; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Tests; |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The target-framework override lets the user hint the TFM used to resolve an assembly's
|
||||||
|
/// references instead of the one detected from its TargetFrameworkAttribute. These tests pin
|
||||||
|
/// the load-bearing behaviour: the override wins over detection, survives the assembly-list
|
||||||
|
/// XML round-trip, defaults to null for assemblies persisted without it, and is carried across
|
||||||
|
/// a reload (which is how a runtime change re-resolves against the new framework).
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture] |
||||||
|
public class TargetFrameworkOverrideTests |
||||||
|
{ |
||||||
|
static AssemblyListManager Manager => AppComposition.Current.GetExport<SettingsService>().AssemblyListManager; |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Override_Wins_Over_Detected_Tfm_Even_After_Detection_Cached() |
||||||
|
{ |
||||||
|
var list = Manager.CreateList("tfm-override-" + Guid.NewGuid().ToString("N")); |
||||||
|
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmOverrideFixture")); |
||||||
|
|
||||||
|
// Force detection to run and cache its result first, so the test also proves the override
|
||||||
|
// takes precedence over an already-cached detected value.
|
||||||
|
var detected = await asm.GetTargetFrameworkIdAsync(); |
||||||
|
asm.TargetFrameworkIdOverride = ".NETFramework,Version=v4.7.2"; |
||||||
|
|
||||||
|
(await asm.GetTargetFrameworkIdAsync()).Should().Be(".NETFramework,Version=v4.7.2"); |
||||||
|
detected.Should().NotBe(".NETFramework,Version=v4.7.2", |
||||||
|
"the fixture is not a .NET Framework 4.7.2 assembly; the override is what changes the result"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public void Override_Survives_SaveAsXml_Roundtrip() |
||||||
|
{ |
||||||
|
var list = Manager.CreateList("tfm-roundtrip-" + Guid.NewGuid().ToString("N")); |
||||||
|
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmRoundtripFixture")); |
||||||
|
asm.TargetFrameworkIdOverride = ".NETCoreApp,Version=v6.0"; |
||||||
|
|
||||||
|
var reloaded = new AssemblyList(Manager, list.SaveAsXml()); |
||||||
|
|
||||||
|
reloaded.GetAssemblies().Should().ContainSingle() |
||||||
|
.Which.TargetFrameworkIdOverride.Should().Be(".NETCoreApp,Version=v6.0"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public void Assembly_Without_Override_Persists_No_Attribute_And_Loads_Null() |
||||||
|
{ |
||||||
|
var list = Manager.CreateList("tfm-noattr-" + Guid.NewGuid().ToString("N")); |
||||||
|
list.OpenAssembly(FixtureAssembly.Emit("TfmNoAttrFixture")); |
||||||
|
|
||||||
|
var xml = list.SaveAsXml(); |
||||||
|
xml.Elements("Assembly").Should().OnlyContain(e => e.Attribute("TargetFramework") == null, |
||||||
|
"no attribute is written when there is no override (backward-compatible XML)"); |
||||||
|
|
||||||
|
var reloaded = new AssemblyList(Manager, xml); |
||||||
|
reloaded.GetAssemblies().Should().ContainSingle() |
||||||
|
.Which.TargetFrameworkIdOverride.Should().BeNull(); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
[TestCase("")] |
||||||
|
[TestCase(" ")] |
||||||
|
public async Task Blank_Override_Normalizes_To_Null_And_Falls_Back_To_Detected(string blank) |
||||||
|
{ |
||||||
|
var list = Manager.CreateList("tfm-blank-" + Guid.NewGuid().ToString("N")); |
||||||
|
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmBlankFixture")); |
||||||
|
var detected = await asm.GetTargetFrameworkIdAsync(); |
||||||
|
|
||||||
|
// A blank/whitespace override must not suppress detection with an empty effective TFM.
|
||||||
|
asm.TargetFrameworkIdOverride = blank; |
||||||
|
|
||||||
|
asm.TargetFrameworkIdOverride.Should().BeNull(); |
||||||
|
(await asm.GetTargetFrameworkIdAsync()).Should().Be(detected); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public void Override_Is_Trimmed() |
||||||
|
{ |
||||||
|
var list = Manager.CreateList("tfm-trim-" + Guid.NewGuid().ToString("N")); |
||||||
|
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmTrimFixture")); |
||||||
|
|
||||||
|
asm.TargetFrameworkIdOverride = " .NETCoreApp,Version=v8.0 "; |
||||||
|
|
||||||
|
asm.TargetFrameworkIdOverride.Should().Be(".NETCoreApp,Version=v8.0"); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Blank_TargetFramework_Attribute_In_Xml_Loads_As_Null() |
||||||
|
{ |
||||||
|
// A hand-edited assembly-list XML may carry TargetFramework="". Loading it must behave
|
||||||
|
// like no override at all, not pin the effective TFM to an empty string.
|
||||||
|
var list = Manager.CreateList("tfm-blankxml-" + Guid.NewGuid().ToString("N")); |
||||||
|
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmBlankXmlFixture")); |
||||||
|
var xml = list.SaveAsXml(); |
||||||
|
xml.Elements("Assembly").Single().SetAttributeValue("TargetFramework", ""); |
||||||
|
|
||||||
|
var reloaded = new AssemblyList(Manager, xml); |
||||||
|
var loaded = reloaded.GetAssemblies().Should().ContainSingle().Subject; |
||||||
|
|
||||||
|
loaded.TargetFrameworkIdOverride.Should().BeNull(); |
||||||
|
(await loaded.GetTargetFrameworkIdAsync()).Should().Be(await asm.GetTargetFrameworkIdAsync()); |
||||||
|
} |
||||||
|
|
||||||
|
[AvaloniaTest] |
||||||
|
public void ReloadAssembly_Carries_Override_To_New_Instance() |
||||||
|
{ |
||||||
|
var list = Manager.CreateList("tfm-reload-" + Guid.NewGuid().ToString("N")); |
||||||
|
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmReloadFixture")); |
||||||
|
asm.TargetFrameworkIdOverride = ".NETStandard,Version=v2.0"; |
||||||
|
|
||||||
|
var reloaded = list.ReloadAssembly(asm); |
||||||
|
|
||||||
|
((object?)reloaded).Should().NotBeNull(); |
||||||
|
reloaded!.Should().NotBeSameAs(asm); |
||||||
|
reloaded.TargetFrameworkIdOverride.Should().Be(".NETStandard,Version=v2.0"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,76 @@ |
|||||||
|
// 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 AwesomeAssertions; |
||||||
|
|
||||||
|
using ICSharpCode.ILSpy.Views; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Tests; |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Set-Target-Framework dialog accepts the short TFMs users know (net48, net6.0, ...) but
|
||||||
|
/// ILSpy resolves with the long FrameworkName form (.NETFramework,Version=v4.8). These tests
|
||||||
|
/// pin that conversion/validation, which is the part of the dialog worth unit-testing in
|
||||||
|
/// isolation from the window.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture] |
||||||
|
public class TargetFrameworkConverterTests |
||||||
|
{ |
||||||
|
[TestCase("net48", ".NETFramework,Version=v4.8")] |
||||||
|
[TestCase("net472", ".NETFramework,Version=v4.7.2")] |
||||||
|
[TestCase("net6.0", ".NETCoreApp,Version=v6.0")] |
||||||
|
[TestCase("net8.0", ".NETCoreApp,Version=v8.0")] |
||||||
|
[TestCase("netstandard2.0", ".NETStandard,Version=v2.0")] |
||||||
|
[TestCase("netcoreapp3.1", ".NETCoreApp,Version=v3.1")] |
||||||
|
public void Short_Tfm_Parses_To_FrameworkName(string input, string expected) |
||||||
|
{ |
||||||
|
TargetFrameworkConverter.TryParseToFrameworkName(input, out var frameworkName, out _) |
||||||
|
.Should().BeTrue(); |
||||||
|
frameworkName.Should().Be(expected); |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void Long_FrameworkName_Is_Also_Accepted() |
||||||
|
{ |
||||||
|
TargetFrameworkConverter.TryParseToFrameworkName(".NETCoreApp,Version=v6.0", out var frameworkName, out _) |
||||||
|
.Should().BeTrue(); |
||||||
|
frameworkName.Should().Be(".NETCoreApp,Version=v6.0"); |
||||||
|
} |
||||||
|
|
||||||
|
[TestCase("")] |
||||||
|
[TestCase(" ")] |
||||||
|
[TestCase("not-a-tfm")] |
||||||
|
[TestCase("garbage123")] |
||||||
|
public void Invalid_Input_Is_Rejected_With_An_Error(string input) |
||||||
|
{ |
||||||
|
TargetFrameworkConverter.TryParseToFrameworkName(input, out var frameworkName, out var error) |
||||||
|
.Should().BeFalse(); |
||||||
|
frameworkName.Should().BeNull(); |
||||||
|
error.Should().NotBeNullOrEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
[TestCase(".NETFramework,Version=v4.8", "net48")] |
||||||
|
[TestCase(".NETCoreApp,Version=v6.0", "net6.0")] |
||||||
|
[TestCase(".NETStandard,Version=v2.0", "netstandard2.0")] |
||||||
|
public void FrameworkName_Converts_To_Short_Folder_Name_For_Display(string frameworkName, string expected) |
||||||
|
{ |
||||||
|
TargetFrameworkConverter.ToShortFolderName(frameworkName).Should().Be(expected); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,133 @@ |
|||||||
|
// 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.Composition; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
using ICSharpCode.ILSpy.AppEnv; |
||||||
|
using ICSharpCode.ILSpy.AssemblyTree; |
||||||
|
using ICSharpCode.ILSpy.Properties; |
||||||
|
using ICSharpCode.ILSpy.TreeNodes; |
||||||
|
using ICSharpCode.ILSpy.Views; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Commands |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Right-click an assembly -> "Set Target Framework". Overrides the framework ILSpy resolves the
|
||||||
|
/// assembly's references against (instead of the one detected from its TargetFrameworkAttribute),
|
||||||
|
/// then reloads so the references re-resolve. Leaving the dialog blank clears the override.
|
||||||
|
/// </summary>
|
||||||
|
[ExportContextMenuEntry(Header = nameof(Resources.SetTargetFramework), Category = "Debug", Order = 420)] |
||||||
|
[Shared] |
||||||
|
public sealed class SetTargetFrameworkContextMenuEntry : IContextMenuEntry |
||||||
|
{ |
||||||
|
readonly AssemblyTreeModel assemblyTreeModel; |
||||||
|
|
||||||
|
[ImportingConstructor] |
||||||
|
public SetTargetFrameworkContextMenuEntry(AssemblyTreeModel assemblyTreeModel) |
||||||
|
{ |
||||||
|
this.assemblyTreeModel = assemblyTreeModel; |
||||||
|
} |
||||||
|
|
||||||
|
public bool IsEnabled(TextViewContext context) => true; |
||||||
|
|
||||||
|
public bool IsVisible(TextViewContext context) |
||||||
|
=> context.SelectedTreeNodes?.Length == 1 |
||||||
|
&& context.SelectedTreeNodes[0] is AssemblyTreeNode asm |
||||||
|
&& asm.LoadedAssembly.IsLoadedAsValidAssembly; |
||||||
|
|
||||||
|
public void Execute(TextViewContext context) |
||||||
|
{ |
||||||
|
if (context.SelectedTreeNodes?.FirstOrDefault() is not AssemblyTreeNode node) |
||||||
|
return; |
||||||
|
ExecuteAsync(node).HandleExceptions(); |
||||||
|
} |
||||||
|
|
||||||
|
async Task ExecuteAsync(AssemblyTreeNode node) |
||||||
|
{ |
||||||
|
var owner = UiContext.MainWindow; |
||||||
|
if (owner == null) |
||||||
|
return; |
||||||
|
var assembly = node.LoadedAssembly; |
||||||
|
var current = await assembly.GetTargetFrameworkIdAsync(); |
||||||
|
|
||||||
|
var result = await new SetTargetFrameworkDialog(current).ShowDialog<string?>(owner); |
||||||
|
if (result == null) |
||||||
|
return; // cancelled
|
||||||
|
var frameworkName = result.Length == 0 ? null : result; |
||||||
|
if (frameworkName == assembly.TargetFrameworkIdOverride |
||||||
|
|| (assembly.TargetFrameworkIdOverride == null && frameworkName == current)) |
||||||
|
return; // unchanged
|
||||||
|
|
||||||
|
ApplyOverride(assemblyTreeModel, node, frameworkName); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the override, reloads so references re-resolve against the new framework, and restores
|
||||||
|
/// the tree selection. Shared by "Set Target Framework" and "Reset Target Framework".
|
||||||
|
/// </summary>
|
||||||
|
internal static void ApplyOverride(AssemblyTreeModel assemblyTreeModel, AssemblyTreeNode node, string? frameworkName) |
||||||
|
{ |
||||||
|
var assembly = node.LoadedAssembly; |
||||||
|
// Snapshot the node path before ReloadAssembly swaps the LoadedAssembly out from under
|
||||||
|
// the tree, then re-select the equivalent node so the user keeps their position.
|
||||||
|
var path = AssemblyTreeModel.GetPathForNode(node); |
||||||
|
assembly.TargetFrameworkIdOverride = frameworkName; |
||||||
|
assembly.AssemblyList.ReloadAssembly(assembly.FileName); |
||||||
|
var restored = assemblyTreeModel.FindNodeByPath(path, returnBestMatch: true); |
||||||
|
if (restored != null) |
||||||
|
assemblyTreeModel.SelectNode(restored); |
||||||
|
assemblyTreeModel.RefreshDecompiledView(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Right-click an assembly that carries a manual target-framework override -> "Reset Target
|
||||||
|
/// Framework". Clears the override and reloads so references re-resolve against the framework
|
||||||
|
/// detected from the TargetFrameworkAttribute again. Only shown while an override is in effect,
|
||||||
|
/// so the user can revert without reopening the dialog.
|
||||||
|
/// </summary>
|
||||||
|
[ExportContextMenuEntry(Header = nameof(Resources.ResetTargetFramework), Category = "Debug", Order = 421)] |
||||||
|
[Shared] |
||||||
|
public sealed class ResetTargetFrameworkContextMenuEntry : IContextMenuEntry |
||||||
|
{ |
||||||
|
readonly AssemblyTreeModel assemblyTreeModel; |
||||||
|
|
||||||
|
[ImportingConstructor] |
||||||
|
public ResetTargetFrameworkContextMenuEntry(AssemblyTreeModel assemblyTreeModel) |
||||||
|
{ |
||||||
|
this.assemblyTreeModel = assemblyTreeModel; |
||||||
|
} |
||||||
|
|
||||||
|
public bool IsEnabled(TextViewContext context) => true; |
||||||
|
|
||||||
|
public bool IsVisible(TextViewContext context) |
||||||
|
=> context.SelectedTreeNodes?.Length == 1 |
||||||
|
&& context.SelectedTreeNodes[0] is AssemblyTreeNode asm |
||||||
|
&& asm.LoadedAssembly.IsLoadedAsValidAssembly |
||||||
|
&& asm.LoadedAssembly.TargetFrameworkIdOverride != null; |
||||||
|
|
||||||
|
public void Execute(TextViewContext context) |
||||||
|
{ |
||||||
|
if (context.SelectedTreeNodes?.FirstOrDefault() is not AssemblyTreeNode node) |
||||||
|
return; |
||||||
|
SetTargetFrameworkContextMenuEntry.ApplyOverride(assemblyTreeModel, node, null); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
x:Class="ICSharpCode.ILSpy.Views.SetTargetFrameworkDialog" |
||||||
|
Width="360" SizeToContent="Height" CanResize="False" |
||||||
|
WindowStartupLocation="CenterOwner" |
||||||
|
Title="ILSpy"> |
||||||
|
<Grid Margin="12,10" RowDefinitions="Auto,Auto,Auto,Auto,Auto"> |
||||||
|
<TextBlock Grid.Row="0" Name="PromptText" Margin="0,0,0,6" TextWrapping="Wrap" /> |
||||||
|
<TextBox Grid.Row="1" Name="FrameworkBox" /> |
||||||
|
<!-- The presets sit in an always-visible list rather than a dropdown: the app forces |
||||||
|
OverlayPopups (see Program.cs), so a popup would be clamped to this small dialog and |
||||||
|
blanket it. A fixed-height ListBox scrolls when the presets overflow. --> |
||||||
|
<ListBox Grid.Row="2" Name="PresetList" Height="150" Margin="0,6,0,0" |
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled" /> |
||||||
|
<TextBlock Grid.Row="3" Name="ErrorText" Margin="0,6,0,0" |
||||||
|
Foreground="Red" TextWrapping="Wrap" IsVisible="False" /> |
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,12,0,0"> |
||||||
|
<Button Name="OkButton" Content="OK" IsDefault="True" MinWidth="72" /> |
||||||
|
<Button Name="CancelButton" Content="Cancel" IsCancel="True" MinWidth="72" /> |
||||||
|
</StackPanel> |
||||||
|
</Grid> |
||||||
|
</Window> |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
// 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.Generic; |
||||||
|
using System.Linq; |
||||||
|
|
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Markup.Xaml; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Views |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Prompts for a target-framework moniker used to override how an assembly's references are
|
||||||
|
/// resolved. A text box accepts the short TFMs users know (net48, net6.0, ...) for free-form
|
||||||
|
/// entry, with the common ones offered in an adjacent list to pick from; input is
|
||||||
|
/// validated/converted to the long FrameworkName form via <see cref="TargetFrameworkConverter"/>.
|
||||||
|
/// Leaving it blank clears the override.
|
||||||
|
/// </summary>
|
||||||
|
public partial class SetTargetFrameworkDialog : Window |
||||||
|
{ |
||||||
|
// Suggestions only; any valid TFM the user types is accepted.
|
||||||
|
static readonly IReadOnlyList<string> CommonFrameworks = new[] { |
||||||
|
"net11.0", "net10.0", "net9.0", "net8.0", "net7.0", "net6.0", "net5.0", |
||||||
|
"netcoreapp3.1", "netcoreapp3.0", "netcoreapp2.1", |
||||||
|
"netstandard2.1", "netstandard2.0", |
||||||
|
"net48", "net472", "net471", "net47", "net462", "net461", "net46", |
||||||
|
}; |
||||||
|
|
||||||
|
TextBox frameworkBox = null!; |
||||||
|
ListBox presetList = null!; |
||||||
|
TextBlock errorText = null!; |
||||||
|
|
||||||
|
public SetTargetFrameworkDialog() |
||||||
|
{ |
||||||
|
InitializeComponent(); |
||||||
|
frameworkBox = this.FindControl<TextBox>("FrameworkBox")!; |
||||||
|
presetList = this.FindControl<ListBox>("PresetList")!; |
||||||
|
errorText = this.FindControl<TextBlock>("ErrorText")!; |
||||||
|
this.FindControl<TextBlock>("PromptText")!.Text = Properties.Resources.TargetFramework; |
||||||
|
presetList.ItemsSource = CommonFrameworks; |
||||||
|
// Picking a preset fills the box; typing stays free-form, so any TFM the user knows is
|
||||||
|
// accepted even when it isn't in the list.
|
||||||
|
presetList.SelectionChanged += (_, _) => { |
||||||
|
if (presetList.SelectedItem is string preset) |
||||||
|
frameworkBox.Text = preset; |
||||||
|
}; |
||||||
|
// Double-clicking a preset is "choose and confirm", like committing a combo selection.
|
||||||
|
presetList.DoubleTapped += (_, _) => { |
||||||
|
if (presetList.SelectedItem is string) |
||||||
|
OnOk(); |
||||||
|
}; |
||||||
|
((Button)this.FindControl<Button>("OkButton")!).Click += (_, _) => OnOk(); |
||||||
|
((Button)this.FindControl<Button>("CancelButton")!).Click += (_, _) => Close(null); |
||||||
|
} |
||||||
|
|
||||||
|
public SetTargetFrameworkDialog(string? currentFrameworkName) : this() |
||||||
|
{ |
||||||
|
Title = Properties.Resources.SetTargetFramework; |
||||||
|
var shortName = TargetFrameworkConverter.ToShortFolderName(currentFrameworkName); |
||||||
|
if (!string.IsNullOrEmpty(shortName)) |
||||||
|
{ |
||||||
|
frameworkBox.Text = shortName; |
||||||
|
// Highlight the matching preset (no-op when the current value isn't one of them).
|
||||||
|
presetList.SelectedItem = CommonFrameworks.FirstOrDefault(f => f == shortName); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void OnOk() |
||||||
|
{ |
||||||
|
var text = frameworkBox.Text; |
||||||
|
if (string.IsNullOrWhiteSpace(text)) |
||||||
|
{ |
||||||
|
// Blank clears the override (revert to auto-detection).
|
||||||
|
Close(string.Empty); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!TargetFrameworkConverter.TryParseToFrameworkName(text, out var frameworkName, out var error)) |
||||||
|
{ |
||||||
|
errorText.Text = error; |
||||||
|
errorText.IsVisible = true; |
||||||
|
return; |
||||||
|
} |
||||||
|
Close(frameworkName); |
||||||
|
} |
||||||
|
|
||||||
|
void InitializeComponent() => AvaloniaXamlLoader.Load(this); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
// 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.Collections.Generic; |
||||||
|
|
||||||
|
using ICSharpCode.ILSpy.Properties; |
||||||
|
|
||||||
|
using NuGet.Frameworks; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Views |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Bridges the short target-framework monikers users type (net48, net6.0, netstandard2.0)
|
||||||
|
/// and the long FrameworkName form ILSpy resolves references with
|
||||||
|
/// (.NETFramework,Version=v4.8). Parsing/validation goes through NuGet so the dialog accepts
|
||||||
|
/// exactly what the ecosystem considers a valid TFM.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TargetFrameworkConverter |
||||||
|
{ |
||||||
|
// NuGet's parser is lenient and coerces arbitrary text into a generic framework, so accept
|
||||||
|
// only identifiers ILSpy's resolver actually understands (NuGet keeps .NET 5+ as .NETCoreApp).
|
||||||
|
static readonly HashSet<string> SupportedIdentifiers = new(StringComparer.OrdinalIgnoreCase) { |
||||||
|
FrameworkConstants.FrameworkIdentifiers.Net, // .NETFramework
|
||||||
|
FrameworkConstants.FrameworkIdentifiers.NetCoreApp, // .NETCoreApp (incl. net5.0+)
|
||||||
|
FrameworkConstants.FrameworkIdentifiers.NetStandard, // .NETStandard
|
||||||
|
}; |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a short TFM (e.g. "net48") or an already-long FrameworkName (e.g.
|
||||||
|
/// ".NETFramework,Version=v4.8") into the long FrameworkName form. Returns false with a
|
||||||
|
/// human-readable <paramref name="error"/> when the input is blank or not a recognised
|
||||||
|
/// framework.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryParseToFrameworkName(string? input, out string? frameworkName, out string? error) |
||||||
|
{ |
||||||
|
frameworkName = null; |
||||||
|
error = null; |
||||||
|
if (string.IsNullOrWhiteSpace(input)) |
||||||
|
{ |
||||||
|
error = Resources.InvalidTargetFramework; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
var framework = TryParse(input.Trim()); |
||||||
|
if (framework == null || framework.IsUnsupported || framework.IsAgnostic || framework.IsAny |
||||||
|
|| !SupportedIdentifiers.Contains(framework.Framework)) |
||||||
|
{ |
||||||
|
error = Resources.InvalidTargetFramework; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
frameworkName = framework.DotNetFrameworkName; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a long FrameworkName to its short folder name for display (e.g.
|
||||||
|
/// ".NETFramework,Version=v4.8" -> "net48"), or returns null if it cannot be parsed.
|
||||||
|
/// </summary>
|
||||||
|
public static string? ToShortFolderName(string? frameworkName) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(frameworkName)) |
||||||
|
return null; |
||||||
|
try |
||||||
|
{ |
||||||
|
var framework = NuGetFramework.ParseFrameworkName(frameworkName, DefaultFrameworkNameProvider.Instance); |
||||||
|
if (framework.IsUnsupported) |
||||||
|
return null; |
||||||
|
return framework.GetShortFolderName(); |
||||||
|
} |
||||||
|
catch (ArgumentException) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Accepts both the short folder form ("net48") and the long FrameworkName form
|
||||||
|
// (".NETFramework,Version=v4.8"); NuGet's two parsers cover one each.
|
||||||
|
static NuGetFramework? TryParse(string input) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var framework = NuGetFramework.Parse(input); |
||||||
|
if (!framework.IsUnsupported) |
||||||
|
return framework; |
||||||
|
} |
||||||
|
catch (ArgumentException) |
||||||
|
{ |
||||||
|
// fall through to the FrameworkName parser
|
||||||
|
} |
||||||
|
try |
||||||
|
{ |
||||||
|
return NuGetFramework.ParseFrameworkName(input, DefaultFrameworkNameProvider.Instance); |
||||||
|
} |
||||||
|
catch (ArgumentException) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue