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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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