diff --git a/ICSharpCode.ILSpyX/Dependencies/UnresolvedReferencesReport.cs b/ICSharpCode.ILSpyX/Dependencies/UnresolvedReferencesReport.cs
new file mode 100644
index 0000000000..35680ce024
--- /dev/null
+++ b/ICSharpCode.ILSpyX/Dependencies/UnresolvedReferencesReport.cs
@@ -0,0 +1,108 @@
+// Copyright (c) 2026 Piero Viano
+//
+// 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 System.Linq;
+using System.Threading;
+
+using ICSharpCode.Decompiler.Metadata;
+
+namespace ICSharpCode.ILSpyX.Dependencies
+{
+ /// An assembly reference that could not be resolved, with its resolution log.
+ public sealed class UnresolvedReference
+ {
+ public UnresolvedReference(AssemblyReference reference, IReadOnlyList<(MessageKind Kind, string Message)> messages)
+ {
+ Reference = reference ?? throw new ArgumentNullException(nameof(reference));
+ Messages = messages ?? throw new ArgumentNullException(nameof(messages));
+ }
+
+ public AssemblyReference Reference { get; }
+
+ /// The probing messages the assembly resolver logged for this reference.
+ public IReadOnlyList<(MessageKind Kind, string Message)> Messages { get; }
+ }
+
+ /// The unresolved references of one referencing assembly.
+ public sealed class UnresolvedReferenceGroup
+ {
+ public UnresolvedReferenceGroup(LoadedAssembly assembly, IReadOnlyList references)
+ {
+ Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
+ References = references ?? throw new ArgumentNullException(nameof(references));
+ }
+
+ public LoadedAssembly Assembly { get; }
+
+ public IReadOnlyList References { get; }
+ }
+
+ ///
+ /// Collects every assembly reference that does not resolve, grouped by the referencing
+ /// assembly. Resolution goes through each assembly's own resolver (the same one the References
+ /// folder uses), so the probing log it records is what the report shows.
+ ///
+ public static class UnresolvedReferencesReport
+ {
+ ///
+ /// Resolves every assembly reference of and returns the groups
+ /// that have at least one unresolved reference, in input order. Assemblies that failed to
+ /// load are skipped. Must not run on the UI thread: resolving may probe the file system.
+ ///
+ public static IReadOnlyList Collect(IEnumerable assemblies,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(assemblies);
+ var groups = new List();
+ foreach (var assembly in assemblies.ToList())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var module = assembly.GetMetadataFileOrNull();
+ if (module == null)
+ continue;
+ var resolver = assembly.GetAssemblyResolver();
+ var unresolved = new List();
+ foreach (var reference in module.AssemblyReferences)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ MetadataFile? resolved;
+ try
+ {
+ resolved = resolver.Resolve(reference);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ resolved = null;
+ }
+ if (resolved != null)
+ continue;
+ IReadOnlyList<(MessageKind, string)> messages =
+ assembly.LoadedAssemblyReferencesInfo.TryGetInfo(reference.FullName, out var info)
+ ? info.Messages.ToList()
+ : Array.Empty<(MessageKind, string)>();
+ unresolved.Add(new UnresolvedReference(reference, messages));
+ }
+ if (unresolved.Count > 0)
+ groups.Add(new UnresolvedReferenceGroup(assembly, unresolved));
+ }
+ return groups;
+ }
+ }
+}
diff --git a/ILSpy.Tests/Dependencies/DependencyFixtures.cs b/ILSpy.Tests/Dependencies/DependencyFixtures.cs
new file mode 100644
index 0000000000..23fef2c1d1
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/DependencyFixtures.cs
@@ -0,0 +1,131 @@
+// Copyright (c) 2026 Piero Viano
+//
+// 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.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.Loader;
+
+namespace ICSharpCode.ILSpy.Tests.Dependencies;
+
+///
+/// Emits small library/consumer assembly pairs for the dependency features. The library defines
+/// Api.Compute(int); the consumer calls it from .,
+/// so it carries an assembly reference to the library.
+///
+static class DependencyFixtures
+{
+ public const string BodyUserType = "UsesInBody";
+ public const string BodyUserMethod = "Run";
+
+ /// A fresh, empty temp directory.
+ public static string NewDirectory()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), $"ILSpyDependencyFixture_{Guid.NewGuid():N}");
+ Directory.CreateDirectory(dir);
+ return dir;
+ }
+
+ /// A unique simple assembly name with the given prefix.
+ public static string UniqueName(string prefix) => prefix + Guid.NewGuid().ToString("N").Substring(0, 8);
+
+ /// Emits the library <directory>/<name>.dll and returns its path.
+ public static string EmitLibrary(string directory, string name)
+ {
+ var assemblyName = new AssemblyName(name) { Version = new Version(1, 0, 0, 0) };
+ var ab = new PersistedAssemblyBuilder(assemblyName, typeof(object).Assembly);
+ var module = ab.DefineDynamicModule(name);
+
+ var api = module.DefineType($"{name}.Api", TypeAttributes.Public | TypeAttributes.Class);
+ var compute = api.DefineMethod("Compute", MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int)]);
+ var il = compute.GetILGenerator();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ret);
+ api.CreateType();
+
+ var path = Path.Combine(directory, name + ".dll");
+ ab.Save(path);
+ return path;
+ }
+
+ ///
+ /// Emits the consumer <directory>/<name>.dll compiled against the library at
+ /// and returns its path. The library is read into a collectible
+ /// load context from memory, so its file is neither locked nor kept loaded.
+ ///
+ public static string EmitConsumer(string directory, string name, string libraryPath)
+ {
+ var context = new AssemblyLoadContext(name, isCollectible: true);
+ try
+ {
+ using var stream = new MemoryStream(File.ReadAllBytes(libraryPath));
+ var library = context.LoadFromStream(stream);
+ string libraryName = library.GetName().Name!;
+ var apiType = library.GetType($"{libraryName}.Api", throwOnError: true)!;
+
+ var ab = new PersistedAssemblyBuilder(new AssemblyName(name) { Version = new Version(1, 0, 0, 0) }, typeof(object).Assembly);
+ var module = ab.DefineDynamicModule(name);
+
+ var bodyUser = module.DefineType($"{name}.{BodyUserType}", TypeAttributes.Public | TypeAttributes.Class);
+ var run = bodyUser.DefineMethod(BodyUserMethod, MethodAttributes.Public | MethodAttributes.Static, typeof(int), Type.EmptyTypes);
+ var il = run.GetILGenerator();
+ il.Emit(OpCodes.Ldc_I4_1);
+ il.Emit(OpCodes.Call, apiType.GetMethod("Compute")!);
+ il.Emit(OpCodes.Ret);
+ bodyUser.CreateType();
+
+ var path = Path.Combine(directory, name + ".dll");
+ ab.Save(path);
+ return path;
+ }
+ finally
+ {
+ context.Unload();
+ }
+ }
+
+ ///
+ /// Emits a library and a consumer next to each other in a fresh directory, so the consumer's
+ /// reference resolves from its own folder.
+ ///
+ public static (string LibraryPath, string ConsumerPath, string LibraryName, string ConsumerName) EmitPair(string prefix = "Dep")
+ {
+ var dir = NewDirectory();
+ var libraryName = UniqueName(prefix + "Lib");
+ var consumerName = UniqueName(prefix + "App");
+ var library = EmitLibrary(dir, libraryName);
+ var consumer = EmitConsumer(dir, consumerName, library);
+ return (library, consumer, libraryName, consumerName);
+ }
+
+ ///
+ /// Emits a consumer whose library reference cannot be resolved: the library is built in a
+ /// separate directory that is deleted afterwards.
+ ///
+ public static (string ConsumerPath, string MissingLibraryName) EmitConsumerWithMissingReference(string prefix = "Dep")
+ {
+ var libraryDir = NewDirectory();
+ var consumerDir = NewDirectory();
+ var libraryName = UniqueName(prefix + "Missing");
+ var library = EmitLibrary(libraryDir, libraryName);
+ var consumer = EmitConsumer(consumerDir, UniqueName(prefix + "Orphan"), library);
+ Directory.Delete(libraryDir, recursive: true);
+ return (consumer, libraryName);
+ }
+}
diff --git a/ILSpy.Tests/Dependencies/UnresolvedReferencesReportTests.cs b/ILSpy.Tests/Dependencies/UnresolvedReferencesReportTests.cs
new file mode 100644
index 0000000000..9246068a5c
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/UnresolvedReferencesReportTests.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2026 Piero Viano
+//
+// 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.Linq;
+using System.Threading.Tasks;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.ILSpyX;
+using ICSharpCode.ILSpyX.Dependencies;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Dependencies;
+
+[TestFixture]
+public class UnresolvedReferencesReportTests
+{
+ [Test]
+ public async Task Missing_Reference_Is_Reported_Under_Its_Referencing_Assembly_With_The_Probe_Log()
+ {
+ var (consumerPath, missingName) = DependencyFixtures.EmitConsumerWithMissingReference("Report");
+ var (libraryPath, resolvableConsumerPath, libraryName, _) = DependencyFixtures.EmitPair("Report");
+ var list = new AssemblyList();
+ var orphan = list.OpenAssembly(consumerPath);
+ var healthy = list.OpenAssembly(resolvableConsumerPath);
+ await orphan.GetLoadResultAsync();
+ await healthy.GetLoadResultAsync();
+
+ var groups = UnresolvedReferencesReport.Collect([orphan, healthy]);
+
+ var group = groups.Single(g => g.Assembly == orphan);
+ var missing = group.References.Single(r => r.Reference.Name == missingName);
+ missing.Messages.Should().Contain(m => m.Kind == MessageKind.Error && m.Message.Contains(missingName),
+ "the resolver's probe log explains why the reference failed");
+ groups.SelectMany(g => g.References).Should().NotContain(r => r.Reference.Name == libraryName,
+ "a reference that resolves is not reported");
+ }
+
+ [Test]
+ public async Task Fully_Resolved_Assemblies_Produce_No_Group()
+ {
+ var (libraryPath, _, _, _) = DependencyFixtures.EmitPair("Report");
+ var list = new AssemblyList();
+ var library = list.OpenAssembly(libraryPath);
+ await library.GetLoadResultAsync();
+
+ // The fixture library only references the core library, which always resolves.
+ UnresolvedReferencesReport.Collect([library]).Should().BeEmpty();
+ }
+}
diff --git a/ILSpy.Tests/Dependencies/UnresolvedReferencesReportUiTests.cs b/ILSpy.Tests/Dependencies/UnresolvedReferencesReportUiTests.cs
new file mode 100644
index 0000000000..c509a3cdc9
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/UnresolvedReferencesReportUiTests.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2026 Piero Viano
+//
+// 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.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.Commands;
+using ICSharpCode.ILSpy.Docking;
+using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TextView;
+using ICSharpCode.ILSpy.TreeNodes;
+using ICSharpCode.ILSpy.ViewModels;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Dependencies;
+
+[TestFixture]
+public class UnresolvedReferencesReportUiTests
+{
+ [AvaloniaTest]
+ public async Task Unresolved_References_Report_Lists_The_Missing_Reference_With_Its_Log()
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ var (consumerPath, missingName) = DependencyFixtures.EmitConsumerWithMissingReference("Ui");
+ var orphan = await vm.OpenAssemblyAsync(consumerPath);
+ vm.AssemblyTreeModel.SelectNode(orphan.ShortName);
+
+ var registry = AppComposition.Current.GetExport();
+ registry.Commands.Single(c => c.Metadata.Header == nameof(Resources.UnresolvedReferencesReport)).Metadata.ParentMenuID
+ .Should().Be(nameof(Resources._Navigate));
+ registry.GetCommand(nameof(Resources.UnresolvedReferencesReport)).Execute(null);
+
+ var dockWorkspace = AppComposition.Current.GetExport();
+ string? text = null;
+ await Waiters.WaitForAsync(() => {
+ var content = dockWorkspace.Documents?.VisibleDockables?.OfType()
+ .Select(t => t.Content).OfType()
+ .FirstOrDefault(c => c.Title == Resources.UnresolvedReferencesReport);
+ text = content?.Text;
+ return text?.Contains(missingName, StringComparison.Ordinal) == true;
+ }, description: "unresolved references report text");
+ text.Should().Contain(orphan.ShortName);
+ text.Should().Contain("Could not find reference");
+ }
+}
diff --git a/ILSpy.Tests/MainWindow/MainMenuTests.cs b/ILSpy.Tests/MainWindow/MainMenuTests.cs
index b770a8e9fd..bca65bb61e 100644
--- a/ILSpy.Tests/MainWindow/MainMenuTests.cs
+++ b/ILSpy.Tests/MainWindow/MainMenuTests.cs
@@ -36,7 +36,7 @@
namespace ICSharpCode.ILSpy.Tests;
-// MainMenu's top-level structure (File / View / Window with mnemonic underscores) is the
+// MainMenu's top-level structure (File / View / Navigate / Window with mnemonic underscores) is the
// scaffolding every later commit hangs items onto via MEF. If a future commit accidentally
// drops one of these top-levels or shuffles the order, the [ExportMainMenuCommand] entries
// that target them by header would silently land in the wrong menu.
@@ -44,7 +44,7 @@ namespace ICSharpCode.ILSpy.Tests;
public class MainMenuTests
{
[AvaloniaTest]
- public void MainMenu_top_level_items_are_File_View_Window_in_order()
+ public void MainMenu_top_level_items_are_File_View_Navigate_Window_in_order()
{
var window = AppComposition.Current.GetExport();
window.Show();
@@ -58,7 +58,7 @@ public void MainMenu_top_level_items_are_File_View_Window_in_order()
// PromoteHelpToMacAppMenu relocates the Help items into the application menu
// (macOS convention: About lives under the bold app-named menu), so _Help is
// not a window-menu top-level there.
- headers.Should().Equal("_File", "_View", "_Window");
+ headers.Should().Equal("_File", "_View", "_Navigate", "_Window");
var appMenu = NativeMenu.GetMenu(Application.Current!);
appMenu.Should().NotBeNull("App.axaml declares the NativeMenu the Help items move into");
@@ -67,7 +67,7 @@ public void MainMenu_top_level_items_are_File_View_Window_in_order()
}
else
{
- headers.Should().Equal("_File", "_View", "_Window", "_Help");
+ headers.Should().Equal("_File", "_View", "_Navigate", "_Window", "_Help");
}
}
@@ -156,7 +156,7 @@ public void Closing_An_Earlier_Window_Leaves_A_Later_Window_Help_Items_In_Place(
// The app menu is declared on Application and outlives every test, so a test that promotes
// placeholder items into it has to take them back out; otherwise a later test reading it
- // (see MainMenu_top_level_items_are_File_View_Window_in_order) sees this test's leftovers.
+ // (see MainMenu_top_level_items_are_File_View_Navigate_Window_in_order) sees this test's leftovers.
static void RestoreAppMenu(NativeMenu appMenu, List promoted)
{
foreach (var item in promoted)
diff --git a/ILSpy/Dependencies/UnresolvedReferencesReportActions.cs b/ILSpy/Dependencies/UnresolvedReferencesReportActions.cs
new file mode 100644
index 0000000000..7c4c41774f
--- /dev/null
+++ b/ILSpy/Dependencies/UnresolvedReferencesReportActions.cs
@@ -0,0 +1,105 @@
+// Copyright (c) 2026 Piero Viano
+//
+// 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 System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using ICSharpCode.Decompiler;
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.ILSpyX;
+using ICSharpCode.ILSpyX.Dependencies;
+using ICSharpCode.ILSpyX.TreeView;
+
+using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.Docking;
+using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TextView;
+using ICSharpCode.ILSpy.TreeNodes;
+
+namespace ICSharpCode.ILSpy.Dependencies
+{
+ ///
+ /// The action behind the unresolved-references report: collect the assembly references that do
+ /// not resolve (off the UI thread) and show them, with their probe logs, in a new document tab.
+ ///
+ public static class UnresolvedReferencesReportActions
+ {
+ ///
+ /// The assemblies the report applies to: the selected assembly nodes (with
+ /// , also the assemblies owning any other selected
+ /// node), or every successfully loaded assembly of when that yields none.
+ ///
+ public static IReadOnlyList GetTargetAssemblies(IEnumerable? selection, AssemblyList? list,
+ bool includeOwnersOfSelection = true)
+ {
+ var selected = (selection ?? Enumerable.Empty())
+ .Select(n => includeOwnersOfSelection
+ ? n.AncestorsAndSelf().OfType().FirstOrDefault()
+ : n as AssemblyTreeNode)
+ .Where(n => n is { LoadedAssembly.IsLoadedAsValidAssembly: true })
+ .Select(n => n!.LoadedAssembly)
+ .Distinct()
+ .ToList();
+ if (selected.Count > 0)
+ return selected;
+ return list?.GetAssemblies().Where(a => a.IsLoadedAsValidAssembly).ToList()
+ ?? (IReadOnlyList)Array.Empty();
+ }
+
+ /// Opens the unresolved-references report for in a new tab.
+ public static Task ShowUnresolvedReferencesReportAsync(IReadOnlyList assemblies)
+ {
+ ArgumentNullException.ThrowIfNull(assemblies);
+ var dockWorkspace = AppComposition.TryGetExport();
+ if (dockWorkspace == null)
+ return Task.CompletedTask;
+ return dockWorkspace.RunInNewTabAsync(Resources.UnresolvedReferencesReport,
+ token => Task.Run(() => CreateUnresolvedReferencesReport(assemblies, token), token));
+ }
+
+ /// Renders the unresolved-references report, grouped by referencing assembly.
+ internal static AvaloniaEditTextOutput CreateUnresolvedReferencesReport(IReadOnlyList assemblies, CancellationToken token)
+ {
+ var groups = UnresolvedReferencesReport.Collect(assemblies, token);
+ var output = new AvaloniaEditTextOutput { Title = Resources.UnresolvedReferencesReport };
+ int total = groups.Sum(g => g.References.Count);
+ output.WriteLine($"{total} unresolved assembly reference(s) in {groups.Count} of {assemblies.Count} assemblies.");
+ output.WriteLine();
+ foreach (var group in groups)
+ {
+ output.WriteLine($"{group.Assembly.ShortName} ({group.Assembly.FileName})");
+ output.Indent();
+ foreach (var unresolved in group.References)
+ {
+ output.WriteLine(unresolved.Reference.FullName);
+ output.Indent();
+ var log = new UnresolvedAssemblyNameReference(unresolved.Reference.FullName);
+ log.Messages.AddRange(unresolved.Messages.Select(m => (m.Kind, m.Message)));
+ AssemblyReferenceTreeNode.PrintAssemblyLoadLogMessages(output, log);
+ output.Unindent();
+ }
+ output.Unindent();
+ output.WriteLine();
+ }
+ return output;
+ }
+ }
+}
diff --git a/ILSpy/Dependencies/UnresolvedReferencesReportCommand.cs b/ILSpy/Dependencies/UnresolvedReferencesReportCommand.cs
new file mode 100644
index 0000000000..6faa454c04
--- /dev/null
+++ b/ILSpy/Dependencies/UnresolvedReferencesReportCommand.cs
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 Piero Viano
+//
+// 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 ICSharpCode.ILSpy.AssemblyTree;
+using ICSharpCode.ILSpy.Commands;
+using ICSharpCode.ILSpy.Properties;
+
+namespace ICSharpCode.ILSpy.Dependencies
+{
+ ///
+ /// Navigate -> "Unresolved References Report" for the selected assembly nodes, or the whole list
+ /// when no assembly node is selected.
+ ///
+ [ExportMainMenuCommand(ParentMenuID = nameof(Resources._Navigate), Header = nameof(Resources.UnresolvedReferencesReport), MenuCategory = "Dependencies", MenuOrder = 620)]
+ [Shared]
+ [method: ImportingConstructor]
+ public sealed class UnresolvedReferencesReportCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand
+ {
+ public override bool CanExecute(object? parameter) => (assemblyTreeModel.AssemblyList?.Count ?? 0) > 0;
+
+ public override void Execute(object? parameter)
+ {
+ var assemblies = UnresolvedReferencesReportActions.GetTargetAssemblies(assemblyTreeModel.SelectedItems, assemblyTreeModel.AssemblyList,
+ includeOwnersOfSelection: false);
+ UnresolvedReferencesReportActions.ShowUnresolvedReferencesReportAsync(assemblies).HandleExceptions();
+ }
+ }
+}
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index a276ef2f82..571f329a27 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -150,6 +150,15 @@ public static string _LoadDependencies {
}
}
+ ///
+ /// Looks up a localized string similar to _Navigate.
+ ///
+ public static string _Navigate {
+ get {
+ return ResourceManager.GetString("_Navigate", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to _New.
///
@@ -3547,6 +3556,15 @@ public static string UILanguage_System {
}
}
+ ///
+ /// Looks up a localized string similar to Unresolved References Report.
+ ///
+ public static string UnresolvedReferencesReport {
+ get {
+ return ResourceManager.GetString("UnresolvedReferencesReport", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to No update for ILSpy found..
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 94e510454b..376272ec41 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -1216,6 +1216,9 @@ Do you want to continue?
System
+
+ Unresolved References Report
+
No update for ILSpy found.
@@ -1303,6 +1306,9 @@ Do you want to continue?
_Load Dependencies
+
+ _Navigate
+
_New
diff --git a/ILSpy/Views/MainMenu.axaml.cs b/ILSpy/Views/MainMenu.axaml.cs
index 710d347009..09a8c2692c 100644
--- a/ILSpy/Views/MainMenu.axaml.cs
+++ b/ILSpy/Views/MainMenu.axaml.cs
@@ -59,15 +59,22 @@ public static void Attach(Window window)
var menu = new NativeMenu();
var topLevelByTag = new Dictionary(StringComparer.Ordinal);
- // Pre-build the three known top-level slots so MEF commands with ParentMenuID set
- // to "_File" / "_View" / "_Window" attach to them. Additional top-level groups
+ // Pre-build the known top-level slots so MEF commands with ParentMenuID set
+ // to "_File" / "_View" / "_Navigate" / "_Window" attach to them. Additional top-level groups
// can still be created later from MEF metadata.
AddTopLevel(menu, topLevelByTag, "_File", nameof(Resources._File));
var viewItem = AddTopLevel(menu, topLevelByTag, "_View", nameof(Resources._View));
+ var navigateItem = AddTopLevel(menu, topLevelByTag, "_Navigate", nameof(Resources._Navigate));
var windowItem = AddTopLevel(menu, topLevelByTag, "_Window", nameof(Resources._Window));
PopulateViewMenu(viewItem.Menu!, settings.SessionSettings, setThemeCommand);
AppendRegistryCommands(menu, topLevelByTag, registry.Commands);
+ // A top-level slot no command attached to would render as an empty, unclickable menu.
+ if (navigateItem.Menu!.Items.Count == 0)
+ {
+ menu.Items.Remove(navigateItem);
+ topLevelByTag.Remove("_Navigate");
+ }
AppendWindowDynamicContent(windowItem.Menu!, dockWorkspace);
if (OperatingSystem.IsMacOS())