Files
ILSpy/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs
Siegfried Pammer 3dfb59ac87 Resolve package entries by identity and end walks nobody reads
Searching a package resolves its entries by file name against a case-insensitive
cache, which is right for an assembly reference but wrong for an archive entry:
two entries differing only in case are two files, and they collapsed onto one
LoadedAssembly, so one was searched twice and the other never. Keying the cache
by the entry itself separates them, and the entry's package-relative path
becomes the assembly's file name, which is what tells the copies of one assembly
in a multi-target package apart wherever a search result shows a location.

Cancellation was only checked between top-level list entries, so a walk the user
had already replaced by typing another character kept extracting package entries
alongside the run they were waiting for. The omnibar had no way to end its run at
all: its view model is per document tab and nothing cancelled it when the tab
went away.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-17 22:39:27 +02:00

256 lines
8.3 KiB
C#

// Copyright (c) 2011 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Util;
using ICSharpCode.ILSpyX.Extensions;
using ICSharpCode.ILSpyX.FileLoaders;
using ICSharpCode.ILSpyX.Instrumentation;
namespace ICSharpCode.ILSpyX
{
class AssemblyListSnapshot
{
readonly ImmutableArray<LoadedAssembly> assemblies;
Dictionary<string, MetadataFile>? asmLookupByFullName;
Dictionary<string, MetadataFile>? asmLookupByShortName;
Dictionary<string, List<(MetadataFile module, Version version)>>? asmLookupByShortNameGrouped;
public ImmutableArray<LoadedAssembly> Assemblies => assemblies;
public AssemblyListSnapshot(ImmutableArray<LoadedAssembly> assemblies)
{
this.assemblies = assemblies;
}
public async Task<MetadataFile?> TryGetModuleAsync(IAssemblyReference reference, string tfm)
{
bool isWinRT = reference.IsWindowsRuntime;
if (tfm.StartsWith(".NETFramework,Version=v4.", StringComparison.Ordinal))
{
tfm = ".NETFramework,Version=v4";
}
string key = tfm + ";" + (isWinRT ? reference.Name : reference.FullName);
var lookup = LazyInit.VolatileRead(ref isWinRT ? ref asmLookupByShortName : ref asmLookupByFullName);
if (lookup == null)
{
lookup = await CreateLoadedAssemblyLookupAsync(shortNames: isWinRT).ConfigureAwait(false);
lookup = LazyInit.GetOrSet(ref isWinRT ? ref asmLookupByShortName : ref asmLookupByFullName, lookup);
}
if (lookup.TryGetValue(key, out MetadataFile? module))
return module;
return null;
}
public async Task<MetadataFile?> TryGetSimilarModuleAsync(IAssemblyReference reference)
{
var lookup = LazyInit.VolatileRead(ref asmLookupByShortNameGrouped);
if (lookup == null)
{
lookup = await CreateLoadedAssemblyShortNameGroupLookupAsync().ConfigureAwait(false);
lookup = LazyInit.GetOrSet(ref asmLookupByShortNameGrouped, lookup);
}
if (!lookup.TryGetValue(reference.Name, out var candidates))
return null;
return candidates.FirstOrDefault(c => c.version >= reference.Version).module ?? candidates.Last().module;
}
private async Task<Dictionary<string, MetadataFile>> CreateLoadedAssemblyLookupAsync(bool shortNames)
{
ILSpyXEventSource.Log.SnapshotLookupBuildStart(assemblies.Length);
try
{
var result = new Dictionary<string, MetadataFile>(StringComparer.OrdinalIgnoreCase);
foreach (LoadedAssembly loaded in assemblies)
{
try
{
var module = await loaded.GetMetadataFileOrNullAsync().ConfigureAwait(false);
if (module == null)
continue;
var reader = module.Metadata;
if (reader == null || !reader.IsAssembly)
continue;
string tfm = await loaded.GetTargetFrameworkIdAsync().ConfigureAwait(false);
if (tfm.StartsWith(".NETFramework,Version=v4.", StringComparison.Ordinal))
{
tfm = ".NETFramework,Version=v4";
}
string key = tfm + ";"
+ (shortNames ? module.Name : module.FullName);
if (!result.ContainsKey(key))
{
result.Add(key, module);
}
}
catch (BadImageFormatException)
{
continue;
}
}
return result;
}
finally
{
ILSpyXEventSource.Log.SnapshotLookupBuildStop(assemblies.Length);
}
}
private async Task<Dictionary<string, List<(MetadataFile module, Version version)>>> CreateLoadedAssemblyShortNameGroupLookupAsync()
{
ILSpyXEventSource.Log.SnapshotLookupBuildStart(assemblies.Length);
try
{
var result = new Dictionary<string, List<(MetadataFile module, Version version)>>(StringComparer.OrdinalIgnoreCase);
foreach (LoadedAssembly loaded in assemblies)
{
try
{
var module = await loaded.GetMetadataFileOrNullAsync().ConfigureAwait(false);
var reader = module?.Metadata;
if (reader == null || !reader.IsAssembly)
continue;
var asmDef = reader.GetAssemblyDefinition();
var asmDefName = reader.GetString(asmDef.Name);
var line = (module!, version: asmDef.Version);
if (!result.TryGetValue(asmDefName, out var existing))
{
existing = new List<(MetadataFile module, Version version)>();
result.Add(asmDefName, existing);
existing.Add(line);
continue;
}
int index = existing.BinarySearch(line.version, l => l.version);
index = index < 0 ? ~index : index + 1;
existing.Insert(index, line);
}
catch (BadImageFormatException)
{
continue;
}
}
return result;
}
finally
{
ILSpyXEventSource.Log.SnapshotLookupBuildStop(assemblies.Length);
}
}
/// <summary>
/// Gets all loaded assemblies recursively, including assemblies found in bundles or packages.
/// </summary>
public async Task<IList<LoadedAssembly>> GetAllAssembliesAsync()
{
var results = new List<LoadedAssembly>(assemblies.Length);
await foreach (var asm in EnumerateAllAssembliesAsync().ConfigureAwait(false))
{
results.Add(asm);
}
return results;
}
/// <summary>
/// Streaming variant of <see cref="GetAllAssembliesAsync"/>: yields each assembly as soon
/// as it is known. Awaiting the load result is what triggers the lazy load, so a consumer
/// that materializes the whole sequence first waits for every assembly on the list to be
/// read off disk before it can do any work.
/// </summary>
public async IAsyncEnumerable<LoadedAssembly> EnumerateAllAssembliesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (var asm in assemblies)
{
cancellationToken.ThrowIfCancellationRequested();
LoadResult? result = null;
try
{
result = await asm.GetLoadResultAsync().ConfigureAwait(false);
}
catch
{
// Load failure: still yield the assembly so the consumer can surface it.
}
if (result == null)
{
yield return asm;
}
else if (result.Package != null)
{
foreach (var descendant in EnumerateDescendants(result.Package.RootFolder, cancellationToken))
{
yield return descendant;
}
}
else if (result.MetadataFile != null)
{
yield return asm;
}
}
static IEnumerable<LoadedAssembly> EnumerateDescendants(PackageFolder folder, CancellationToken cancellationToken)
{
foreach (var subFolder in folder.Folders)
{
foreach (var descendant in EnumerateDescendants(subFolder, cancellationToken))
{
yield return descendant;
}
}
foreach (var entry in folder.Entries)
{
// Checked per entry, not per package: resolving one starts extracting it from
// the archive, so a walk the consumer has given up on must stop expanding.
cancellationToken.ThrowIfCancellationRequested();
if (!entry.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !entry.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
continue;
LoadedAssembly? asm;
try
{
asm = folder.ResolveEntry(entry);
}
catch
{
// One unreadable entry must not abandon the rest of the package.
continue;
}
if (asm == null)
continue;
yield return asm;
}
}
}
}
}