Browse Source

Fix Load Dependencies dropping the assemblies it resolved

The command resolves each selected assembly's references through the
per-assembly resolver, which adds the targets to the live list as
auto-loaded entries. It then finished with a full list Refresh (F5),
whose LoadList rebuilds the list from persisted state -- which never
contains on-demand auto-loaded assemblies -- so the just-resolved
dependencies were discarded the instant they were added and the command
looked like a no-op. Re-decompile the active view instead (the original
behaviour) without reloading the list. F5 still drops auto-loaded
assemblies by design; the two refresh paths must stay distinct.
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
795a3be8ff
  1. 51
      ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs
  2. 34
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  3. 14
      ILSpy/TreeNodes/AssemblyTreeNode.cs

51
ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs

@ -1659,4 +1659,55 @@ public class AssemblyTreeTests
ReferenceEquals(modelSelection[0], targetNode).Should().BeTrue( ReferenceEquals(modelSelection[0], targetNode).Should().BeTrue(
"the surviving selection must be the row the user clicked"); "the surviving selection must be the row the user clicked");
} }
[AvaloniaTest]
public async Task Load_Dependencies_Resolves_References_And_Keeps_Them_In_The_List()
{
// "Load Dependencies" must resolve every reference of the selected assembly and leave
// the resolved (auto-loaded) assemblies in the list. Regression: the command used to
// finish with a full list Refresh (F5), which rebuilds the list from persisted state and
// silently drops the just-resolved auto-loaded dependencies -- so the command appeared
// to do nothing.
// Arrange -- boot, open System.Net.Http (it references many assemblies not in the list).
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location;
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var openCommand = registry.Commands
.Single(c => c.Metadata.Header == nameof(Resources._Open))
.CreateExport().Value;
openCommand.Execute(newAsmPath);
await Waiters.WaitForAsync(() =>
vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a =>
string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)));
var httpNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(
System.IO.Path.GetFileNameWithoutExtension(newAsmPath));
await httpNode.LoadedAssembly.GetLoadResultAsync();
var before = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies()
.Select(a => a.FileName)
.ToHashSet(System.StringComparer.OrdinalIgnoreCase);
// Act -- run Load Dependencies on the System.Net.Http node.
await vm.AssemblyTreeModel.LoadDependenciesAsync(new SharpTreeNode[] { httpNode });
for (int i = 0; i < 8; i++)
{
Dispatcher.UIThread.RunJobs();
await Task.Delay(25);
}
// Assert -- references were resolved AND survive in the list as auto-loaded entries.
var added = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies()
.Where(a => !before.Contains(a.FileName))
.ToList();
added.Should().NotBeEmpty(
"Load Dependencies must resolve referenced assemblies and keep them in the list");
added.Should().OnlyContain(a => a.IsAutoLoaded,
"freshly resolved dependencies are auto-loaded");
}
} }

34
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -928,6 +928,40 @@ namespace ILSpy.AssemblyTree
} }
} }
/// <summary>
/// Re-runs decompilation of the active tab WITHOUT reloading the assembly list. Mirrors
/// WPF's RefreshDecompiledView(). Unlike <see cref="Refresh"/> (F5), this must not rebuild
/// the list from persisted state -- that would discard on-demand auto-loaded assemblies
/// (e.g. the ones <see cref="LoadDependenciesAsync"/> just resolved).
/// </summary>
public void RefreshDecompiledView()
=> TryGetExport<Docking.DockWorkspace>()?.ForceRefreshActiveTab();
/// <summary>
/// Resolves every assembly reference of each supplied assembly node through that
/// assembly's own resolver -- which auto-loads the targets into the live list -- then
/// re-decompiles the active tab so newly available references render. Mirrors WPF's
/// LoadDependencies command.
/// </summary>
public async Task LoadDependenciesAsync(IReadOnlyList<SharpTreeNode> nodes)
{
var tasks = new List<Task>();
foreach (var node in nodes)
{
if (node is not AssemblyTreeNode { LoadedAssembly: { } la })
continue;
var resolver = la.GetAssemblyResolver();
var module = la.GetMetadataFileOrNull();
if (module is null)
continue;
foreach (var assyRef in module.Metadata.AssemblyReferences)
tasks.Add(resolver.ResolveAsync(
new ICSharpCode.Decompiler.Metadata.AssemblyReference(module, assyRef)));
}
await Task.WhenAll(tasks);
RefreshDecompiledView();
}
async Task RefreshInternalAsync() async Task RefreshInternalAsync()
{ {
if (AssemblyList == null || listManager == null) if (AssemblyList == null || listManager == null)

14
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -623,19 +623,7 @@ namespace ILSpy.TreeNodes
{ {
if (context.SelectedTreeNodes == null) if (context.SelectedTreeNodes == null)
return; return;
var tasks = new List<Task>(); await assemblyTreeModel.LoadDependenciesAsync(context.SelectedTreeNodes);
foreach (var node in context.SelectedTreeNodes)
{
var la = ((AssemblyTreeNode)node).LoadedAssembly;
var resolver = la.GetAssemblyResolver();
var module = la.GetMetadataFileOrNull();
if (module is null)
continue;
foreach (var assyRef in module.Metadata.AssemblyReferences)
tasks.Add(resolver.ResolveAsync(new AssemblyReference(module, assyRef)));
}
await Task.WhenAll(tasks);
assemblyTreeModel.Refresh();
} }
} }
} }

Loading…
Cancel
Save