diff --git a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs
index 1b911b57c8c..330994fa26b 100644
--- a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs
+++ b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs
@@ -139,6 +139,44 @@ public static bool TryGetFullAssemblyName(this SRM.AssemblyReference reference,
}
}
+ ///
+ /// True when names the assembly defined by :
+ /// same simple name (ignoring case), culture and public key token. The version is not compared,
+ /// since binding redirects and unified frameworks satisfy references with other versions.
+ ///
+ public static bool IsReferenceTo(this IAssemblyReference reference, MetadataReader reader)
+ {
+ if (reference == null)
+ throw new ArgumentNullException(nameof(reference));
+ if (reader == null)
+ throw new ArgumentNullException(nameof(reader));
+ if (!reader.IsAssembly)
+ return false;
+ var asm = reader.GetAssemblyDefinition();
+ if (!string.Equals(reference.Name, reader.GetString(asm.Name), StringComparison.OrdinalIgnoreCase))
+ return false;
+ if (!string.Equals(NormalizeCulture(reference.Culture), NormalizeCulture(reader.GetString(asm.Culture)), StringComparison.OrdinalIgnoreCase))
+ return false;
+ var token = reference.PublicKeyToken is { Length: > 0 } bytes ? bytes.ToHexString(bytes.Length) : "null";
+ return string.Equals(token, reader.GetPublicKeyToken(), StringComparison.OrdinalIgnoreCase);
+
+ static string NormalizeCulture(string culture)
+ => string.IsNullOrEmpty(culture) || culture == "neutral" ? string.Empty : culture;
+ }
+
+ ///
+ /// True when asks for a different version than the assembly
+ /// defined by . Unknown versions never count as a mismatch.
+ ///
+ public static bool IsVersionMismatch(this IAssemblyReference reference, MetadataReader reader)
+ {
+ if (reference == null)
+ throw new ArgumentNullException(nameof(reference));
+ if (reader == null)
+ throw new ArgumentNullException(nameof(reader));
+ return reference.Version != null && reader.IsAssembly && reference.Version != reader.GetAssemblyDefinition().Version;
+ }
+
public static string ToHexString(this IEnumerable bytes, int estimatedLength)
{
if (bytes == null)
diff --git a/ICSharpCode.ILSpyX/Analyzers/Builtin/ModuleDependentCodeAnalyzer.cs b/ICSharpCode.ILSpyX/Analyzers/Builtin/ModuleDependentCodeAnalyzer.cs
new file mode 100644
index 00000000000..f06a134ecef
--- /dev/null
+++ b/ICSharpCode.ILSpyX/Analyzers/Builtin/ModuleDependentCodeAnalyzer.cs
@@ -0,0 +1,68 @@
+// 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.Composition;
+
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpyX.Dependencies;
+
+namespace ICSharpCode.ILSpyX.Analyzers.Builtin
+{
+ ///
+ /// For an assembly, lists the code in the other assemblies of the list that depends on it: every
+ /// type or member whose signature, base types, attributes or body uses a type or member of the
+ /// analyzed assembly. Results are grouped by referencing assembly (assemblies in list order).
+ ///
+ [ExportAnalyzer(Header = "Dependent Code", Order = 20)]
+ [Shared]
+ class ModuleDependentCodeAnalyzer : IAnalyzer
+ {
+ public bool Show(ISymbol? symbol) => symbol is IModule { MetadataFile.IsAssembly: true };
+
+ public IEnumerable Analyze(ISymbol analyzedSymbol, AnalyzerContext context)
+ {
+ ArgumentNullException.ThrowIfNull(analyzedSymbol);
+ ArgumentNullException.ThrowIfNull(context);
+ if (analyzedSymbol is not IModule { MetadataFile: { } target })
+ throw new ArgumentException("A module with metadata is required.", nameof(analyzedSymbol));
+ return AnalyzeCore(target, context);
+ }
+
+ static IEnumerable AnalyzeCore(MetadataFile target, AnalyzerContext context)
+ {
+ var ct = context.CancellationToken;
+ var assemblies = context.AssemblyList.GetAllAssemblies().GetAwaiter().GetResult();
+ foreach (var assembly in assemblies)
+ {
+ ct.ThrowIfCancellationRequested();
+ var module = assembly.GetMetadataFileOrNull();
+ if (module == null || module.IsMetadataOnly || ReferenceEquals(module, target))
+ continue;
+ var references = DependentCodeScanner.FindReferencesTo(module, target);
+ if (references.Count == 0)
+ continue;
+ var typeSystem = context.GetOrCreateTypeSystem(module);
+ foreach (var symbol in DependentCodeScanner.FindDependentSymbols(module, typeSystem, references, ct))
+ yield return symbol;
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.ILSpyX/Analyzers/Builtin/ModuleReferencedByAnalyzer.cs b/ICSharpCode.ILSpyX/Analyzers/Builtin/ModuleReferencedByAnalyzer.cs
new file mode 100644
index 00000000000..fde248ba408
--- /dev/null
+++ b/ICSharpCode.ILSpyX/Analyzers/Builtin/ModuleReferencedByAnalyzer.cs
@@ -0,0 +1,59 @@
+// 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.Composition;
+
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpyX.Dependencies;
+
+namespace ICSharpCode.ILSpyX.Analyzers.Builtin
+{
+ ///
+ /// For an assembly, lists the assemblies of the list that reference it (by name and public key
+ /// token; the referenced version may differ). Each result is itself a module, so it can be
+ /// expanded to follow the chain of referencing assemblies further.
+ ///
+ [ExportAnalyzer(Header = "Referenced By", Order = 10)]
+ [Shared]
+ class ModuleReferencedByAnalyzer : IAnalyzer
+ {
+ public bool Show(ISymbol? symbol) => symbol is IModule { MetadataFile.IsAssembly: true };
+
+ public IEnumerable Analyze(ISymbol analyzedSymbol, AnalyzerContext context)
+ {
+ ArgumentNullException.ThrowIfNull(analyzedSymbol);
+ ArgumentNullException.ThrowIfNull(context);
+ if (analyzedSymbol is not IModule { MetadataFile: { } target })
+ throw new ArgumentException("A module with metadata is required.", nameof(analyzedSymbol));
+ return AnalyzeCore(target, context);
+ }
+
+ static IEnumerable AnalyzeCore(MetadataFile target, AnalyzerContext context)
+ {
+ var assemblies = context.AssemblyList.GetAllAssemblies().GetAwaiter().GetResult();
+ foreach (var referencing in ReferencedByFinder.FindReferencingAssemblies(assemblies, target, context.CancellationToken))
+ {
+ context.CancellationToken.ThrowIfCancellationRequested();
+ yield return context.GetOrCreateTypeSystem(referencing.Module).MainModule;
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.ILSpyX/Dependencies/DependentCodeScanner.cs b/ICSharpCode.ILSpyX/Dependencies/DependentCodeScanner.cs
new file mode 100644
index 00000000000..44020c966d4
--- /dev/null
+++ b/ICSharpCode.ILSpyX/Dependencies/DependentCodeScanner.cs
@@ -0,0 +1,437 @@
+// 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.Collections.Immutable;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Threading;
+
+using ICSharpCode.Decompiler;
+using ICSharpCode.Decompiler.Disassembler;
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpyX.Analyzers;
+
+namespace ICSharpCode.ILSpyX.Dependencies
+{
+ ///
+ /// Finds the code of one module that depends on another assembly: the types, methods, fields,
+ /// properties and events whose signatures, base types, interfaces, generic constraints, custom
+ /// attributes (including those on parameters, and the types named in their arguments), locals
+ /// or IL operands mention a type or member whose resolution scope is one of a given set of
+ /// assembly references. Works on raw metadata, so an unresolvable target still counts as a
+ /// dependency; only the underlying types of enums in attribute arguments are resolved.
+ ///
+ sealed class DependentCodeScanner : ISignatureTypeProvider
+ {
+ readonly MetadataFile module;
+ readonly MetadataReader metadata;
+ readonly HashSet targetReferences;
+ readonly List targetIdentities;
+ readonly Dictionary typeReferenceCache = new();
+ readonly Dictionary typeSpecificationCache = new();
+
+ DependentCodeScanner(MetadataFile module, IEnumerable targetReferences)
+ {
+ this.module = module;
+ this.metadata = module.Metadata;
+ this.targetReferences = new HashSet(targetReferences);
+ this.targetIdentities = module.AssemblyReferences
+ .Where(r => this.targetReferences.Contains(r.Handle))
+ .ToList();
+ }
+
+ ///
+ /// The assembly references of that denote
+ /// (same name, culture and public key token).
+ ///
+ public static IReadOnlyList FindReferencesTo(MetadataFile module, MetadataFile target)
+ {
+ return module.AssemblyReferences
+ .Where(r => r.IsReferenceTo(target.Metadata))
+ .Select(r => r.Handle)
+ .ToList();
+ }
+
+ ///
+ /// Entities of 's main module (which must be built over
+ /// ) that depend on any of ,
+ /// each reported once, in metadata order (types, then members, then attribute owners).
+ /// Accessors are reported as their owning property or event, parameters as their method.
+ ///
+ public static IEnumerable FindDependentSymbols(MetadataFile module, DecompilerTypeSystem typeSystem,
+ IReadOnlyCollection targetReferences, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(module);
+ ArgumentNullException.ThrowIfNull(typeSystem);
+ ArgumentNullException.ThrowIfNull(targetReferences);
+ if (targetReferences.Count == 0)
+ return Array.Empty();
+ var scanner = new DependentCodeScanner(module, targetReferences);
+ return scanner.Scan(typeSystem.MainModule, typeSystem, cancellationToken).Distinct();
+ }
+
+ IEnumerable Scan(MetadataModule mainModule, DecompilerTypeSystem typeSystem, CancellationToken ct)
+ {
+ foreach (var h in metadata.TypeDefinitions)
+ {
+ ct.ThrowIfCancellationRequested();
+ var td = metadata.GetTypeDefinition(h);
+ bool found = UsesTarget(td.BaseType);
+ foreach (var ih in td.GetInterfaceImplementations())
+ found |= UsesTarget(metadata.GetInterfaceImplementation(ih).Interface);
+ found |= UsesTargetInConstraints(td.GetGenericParameters());
+ if (found && mainModule.GetDefinition(h) is { } type)
+ yield return type;
+ }
+
+ foreach (var h in metadata.MethodDefinitions)
+ {
+ ct.ThrowIfCancellationRequested();
+ var md = metadata.GetMethodDefinition(h);
+ bool found = Safe(() => AnyInSignature(md.DecodeSignature(this, null)))
+ || UsesTargetInConstraints(md.GetGenericParameters())
+ || UsesTargetInBody(md);
+ if (found && mainModule.GetDefinition(h) is { } method)
+ yield return method.AccessorOwner ?? method;
+ }
+
+ foreach (var h in metadata.FieldDefinitions)
+ {
+ ct.ThrowIfCancellationRequested();
+ var fd = metadata.GetFieldDefinition(h);
+ if (Safe(() => fd.DecodeSignature(this, null)) && mainModule.GetDefinition(h) is { } field)
+ yield return field;
+ }
+
+ foreach (var h in metadata.PropertyDefinitions)
+ {
+ ct.ThrowIfCancellationRequested();
+ var pd = metadata.GetPropertyDefinition(h);
+ if (Safe(() => AnyInSignature(pd.DecodeSignature(this, null))) && mainModule.GetDefinition(h) is { } property)
+ yield return property;
+ }
+
+ foreach (var h in metadata.EventDefinitions)
+ {
+ ct.ThrowIfCancellationRequested();
+ var ed = metadata.GetEventDefinition(h);
+ if (UsesTarget(ed.Type) && mainModule.GetDefinition(h) is { } ev)
+ yield return ev;
+ }
+
+ var argumentDecoder = new AttributeArgumentDecoder(this, mainModule);
+ var referencedParameters = new HashSet();
+ foreach (var h in metadata.CustomAttributes)
+ {
+ ct.ThrowIfCancellationRequested();
+ var attribute = metadata.GetCustomAttribute(h);
+ if (!UsesTargetInAttributeConstructor(attribute.Constructor) && !UsesTargetInArguments(attribute, argumentDecoder))
+ continue;
+ if (attribute.Parent.Kind == HandleKind.Parameter)
+ {
+ referencedParameters.Add((ParameterHandle)attribute.Parent);
+ continue;
+ }
+ var parent = AnalyzerHelpers.GetParentEntity(typeSystem, attribute);
+ if (parent != null)
+ yield return parent;
+ }
+
+ if (referencedParameters.Count == 0)
+ yield break;
+ foreach (var h in metadata.MethodDefinitions)
+ {
+ ct.ThrowIfCancellationRequested();
+ if (metadata.GetMethodDefinition(h).GetParameters().Any(referencedParameters.Contains)
+ && mainModule.GetDefinition(h) is { } method)
+ {
+ yield return method.AccessorOwner ?? method;
+ }
+ }
+ }
+
+ ///
+ /// True when the attribute constructor belongs to the target, or is a
+ /// constructor of this module whose signature mentions the target (e.g. an enum parameter).
+ ///
+ bool UsesTargetInAttributeConstructor(EntityHandle ctor)
+ {
+ if (ctor.Kind != HandleKind.MethodDefinition)
+ return UsesTarget(ctor);
+ var md = metadata.GetMethodDefinition((MethodDefinitionHandle)ctor);
+ return Safe(() => AnyInSignature(md.DecodeSignature(this, null)));
+ }
+
+ /// True when an argument of is or names a type of the target.
+ static bool UsesTargetInArguments(CustomAttribute attribute, AttributeArgumentDecoder decoder)
+ {
+ return Safe(() => {
+ var value = attribute.DecodeValue(decoder);
+ return value.FixedArguments.Any(IsTargetArgument)
+ || value.NamedArguments.Any(a => IsTargetArgument(new(a.Type, a.Value)));
+ });
+
+ static bool IsTargetArgument(CustomAttributeTypedArgument argument)
+ => argument.Type.UsesTarget
+ || argument.Value is AttributeArgumentType { UsesTarget: true }
+ || (argument.Value is ImmutableArray> elements
+ && elements.Any(IsTargetArgument));
+ }
+
+ /// True when a parsed serialized type name names a type of the target, or is built from one.
+ bool IsTargetTypeName(TypeName name)
+ {
+ if (name.IsArray || name.IsPointer || name.IsByRef)
+ return IsTargetTypeName(name.GetElementType());
+ if (name.IsConstructedGenericType)
+ return IsTargetTypeName(name.GetGenericTypeDefinition()) || name.GetGenericArguments().Any(IsTargetTypeName);
+ if (name.AssemblyName is not { } assemblyName)
+ return false;
+ var parsed = assemblyName.ToAssemblyName();
+ return targetIdentities.Any(target =>
+ string.Equals(target.Name, parsed.Name, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(NormalizeCulture(target.Culture), NormalizeCulture(parsed.CultureName), StringComparison.OrdinalIgnoreCase)
+ && (target.PublicKeyToken ?? []).AsSpan().SequenceEqual(parsed.GetPublicKeyToken() ?? []));
+
+ static string NormalizeCulture(string? culture)
+ => string.IsNullOrEmpty(culture) || culture == "neutral" ? string.Empty : culture;
+ }
+
+ static bool Safe(Func check)
+ {
+ try
+ {
+ return check();
+ }
+ catch (BadImageFormatException)
+ {
+ return false;
+ }
+ catch (EnumUnderlyingTypeResolveException)
+ {
+ return false;
+ }
+ }
+
+ static bool AnyInSignature(MethodSignature signature)
+ => signature.ReturnType || signature.ParameterTypes.Any(p => p);
+
+ bool UsesTargetInConstraints(GenericParameterHandleCollection parameters)
+ {
+ foreach (var h in parameters)
+ {
+ var gp = metadata.GetGenericParameter(h);
+ foreach (var ch in gp.GetConstraints())
+ {
+ if (UsesTarget(metadata.GetGenericParameterConstraint(ch).Type))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool UsesTargetInBody(MethodDefinition md)
+ {
+ if (md.RelativeVirtualAddress == 0)
+ return false;
+ MethodBodyBlock body;
+ try
+ {
+ body = module.GetMethodBody(md.RelativeVirtualAddress);
+ }
+ catch (BadImageFormatException)
+ {
+ return false;
+ }
+ if (!body.LocalSignature.IsNil && UsesTarget(body.LocalSignature))
+ return true;
+ var reader = body.GetILReader();
+ try
+ {
+ while (reader.RemainingBytes > 0)
+ {
+ var opCode = reader.DecodeOpCode();
+ switch (opCode.GetOperandType())
+ {
+ case OperandType.Field:
+ case OperandType.Method:
+ case OperandType.Sig:
+ case OperandType.Tok:
+ case OperandType.Type:
+ if (UsesTarget(MetadataTokenHelpers.EntityHandleOrNil(reader.ReadInt32())))
+ return true;
+ break;
+ default:
+ reader.SkipOperand(opCode);
+ break;
+ }
+ }
+ }
+ catch (BadImageFormatException)
+ {
+ // A truncated or malformed body is scanned up to the point where it breaks.
+ }
+ return false;
+ }
+
+ ///
+ /// True when (a type or member reference, a type or method
+ /// specification, or a standalone signature) mentions the target assembly.
+ ///
+ bool UsesTarget(EntityHandle handle)
+ {
+ if (handle.IsNil)
+ return false;
+ return Safe(() => {
+ switch (handle.Kind)
+ {
+ case HandleKind.TypeReference:
+ return IsTargetTypeReference((TypeReferenceHandle)handle);
+ case HandleKind.TypeSpecification:
+ return IsTargetTypeSpecification((TypeSpecificationHandle)handle);
+ case HandleKind.MemberReference:
+ var mr = metadata.GetMemberReference((MemberReferenceHandle)handle);
+ if (UsesTarget(mr.Parent))
+ return true;
+ return mr.GetKind() switch {
+ MemberReferenceKind.Method => AnyInSignature(mr.DecodeMethodSignature(this, null)),
+ MemberReferenceKind.Field => mr.DecodeFieldSignature(this, null),
+ _ => false,
+ };
+ case HandleKind.MethodSpecification:
+ var ms = metadata.GetMethodSpecification((MethodSpecificationHandle)handle);
+ return UsesTarget(ms.Method) || ms.DecodeSignature(this, null).Any(t => t);
+ case HandleKind.StandaloneSignature:
+ var ss = metadata.GetStandaloneSignature((StandaloneSignatureHandle)handle);
+ return ss.GetKind() switch {
+ StandaloneSignatureKind.Method => AnyInSignature(ss.DecodeMethodSignature(this, null)),
+ StandaloneSignatureKind.LocalVariables => ss.DecodeLocalSignature(this, null).Any(t => t),
+ _ => false,
+ };
+ default:
+ // Definitions in this module and module references are not the target.
+ return false;
+ }
+ });
+ }
+
+ bool IsTargetTypeReference(TypeReferenceHandle handle)
+ {
+ if (typeReferenceCache.TryGetValue(handle, out bool cached))
+ return cached;
+ var scope = metadata.GetTypeReference(handle).ResolutionScope;
+ bool result = scope.Kind switch {
+ HandleKind.AssemblyReference => targetReferences.Contains((AssemblyReferenceHandle)scope),
+ HandleKind.TypeReference => IsTargetTypeReference((TypeReferenceHandle)scope),
+ _ => false,
+ };
+ typeReferenceCache[handle] = result;
+ return result;
+ }
+
+ bool IsTargetTypeSpecification(TypeSpecificationHandle handle)
+ {
+ if (typeSpecificationCache.TryGetValue(handle, out bool cached))
+ return cached;
+ // Guards against a (malformed) self-referencing specification while it is decoded.
+ typeSpecificationCache[handle] = false;
+ bool result = metadata.GetTypeSpecification(handle).DecodeSignature(this, null);
+ typeSpecificationCache[handle] = result;
+ return result;
+ }
+
+ ///
+ /// An attribute argument type as seen by : whether it
+ /// mentions the target, the underlying type when it is an enum, and whether it is System.Type.
+ ///
+ readonly record struct AttributeArgumentType(bool UsesTarget, PrimitiveTypeCode EnumUnderlyingType = 0, bool IsSystemType = false);
+
+ ///
+ /// Decodes custom attribute blobs into s. Target detection
+ /// stays on raw metadata; enum underlying types, which the blob format requires, are resolved
+ /// through the type system.
+ ///
+ sealed class AttributeArgumentDecoder(DependentCodeScanner scanner, MetadataModule mainModule)
+ : ICustomAttributeTypeProvider
+ {
+ public AttributeArgumentType GetPrimitiveType(PrimitiveTypeCode typeCode) => new(false);
+ public AttributeArgumentType GetSystemType() => new(false, IsSystemType: true);
+ public AttributeArgumentType GetSZArrayType(AttributeArgumentType elementType) => new(elementType.UsesTarget);
+ public bool IsSystemType(AttributeArgumentType type) => type.IsSystemType;
+
+ public AttributeArgumentType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
+ {
+ if (handle.IsEnum(reader, out PrimitiveTypeCode underlying))
+ return new(false, underlying);
+ return new(false, IsSystemType: ((EntityHandle)handle).IsKnownType(reader, KnownTypeCode.Type));
+ }
+
+ public AttributeArgumentType GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
+ {
+ if (((EntityHandle)handle).IsKnownType(reader, KnownTypeCode.Type))
+ return new(false, IsSystemType: true);
+ return new(scanner.IsTargetTypeReference(handle), EnumUnderlyingTypeOf(mainModule.ResolveType(handle, default)));
+ }
+
+ public AttributeArgumentType GetTypeFromSerializedName(string name)
+ {
+ if (!TypeName.TryParse(name.AsSpan(), out var parsed))
+ return new(false);
+ IType? type;
+ try
+ {
+ type = ReflectionHelper.ParseReflectionName(name, new SimpleTypeResolveContext(mainModule));
+ }
+ catch (ReflectionNameParseException)
+ {
+ type = null;
+ }
+ return new(scanner.IsTargetTypeName(parsed), EnumUnderlyingTypeOf(type));
+ }
+
+ public PrimitiveTypeCode GetUnderlyingEnumType(AttributeArgumentType type)
+ => type.EnumUnderlyingType != 0 ? type.EnumUnderlyingType : throw new EnumUnderlyingTypeResolveException();
+
+ static PrimitiveTypeCode EnumUnderlyingTypeOf(IType? type)
+ => type?.GetDefinition()?.EnumUnderlyingType?.GetDefinition()?.KnownTypeCode.ToPrimitiveTypeCode() ?? 0;
+ }
+
+ #region ISignatureTypeProvider
+ public bool GetArrayType(bool elementType, ArrayShape shape) => elementType;
+ public bool GetByReferenceType(bool elementType) => elementType;
+ public bool GetFunctionPointerType(MethodSignature signature) => AnyInSignature(signature);
+ public bool GetGenericInstantiation(bool genericType, ImmutableArray typeArguments)
+ => genericType || typeArguments.Any(t => t);
+ public bool GetGenericMethodParameter(object? genericContext, int index) => false;
+ public bool GetGenericTypeParameter(object? genericContext, int index) => false;
+ public bool GetModifiedType(bool modifier, bool unmodifiedType, bool isRequired) => modifier || unmodifiedType;
+ public bool GetPinnedType(bool elementType) => elementType;
+ public bool GetPointerType(bool elementType) => elementType;
+ public bool GetPrimitiveType(PrimitiveTypeCode typeCode) => false;
+ public bool GetSZArrayType(bool elementType) => elementType;
+ public bool GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => false;
+ public bool GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
+ => IsTargetTypeReference(handle);
+ public bool GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind)
+ => IsTargetTypeSpecification(handle);
+ #endregion
+ }
+}
diff --git a/ICSharpCode.ILSpyX/Dependencies/ReferencedByFinder.cs b/ICSharpCode.ILSpyX/Dependencies/ReferencedByFinder.cs
new file mode 100644
index 00000000000..0cfc8037d51
--- /dev/null
+++ b/ICSharpCode.ILSpyX/Dependencies/ReferencedByFinder.cs
@@ -0,0 +1,99 @@
+// 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
+{
+ ///
+ /// One loaded assembly that references a given target assembly, together with the reference
+ /// row that names it.
+ ///
+ public sealed class ReferencingAssembly
+ {
+ public ReferencingAssembly(LoadedAssembly assembly, MetadataFile module, AssemblyReference reference, bool isVersionMismatch)
+ {
+ Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
+ Module = module ?? throw new ArgumentNullException(nameof(module));
+ Reference = reference ?? throw new ArgumentNullException(nameof(reference));
+ IsVersionMismatch = isVersionMismatch;
+ }
+
+ /// The referencing assembly.
+ public LoadedAssembly Assembly { get; }
+
+ /// The metadata of .
+ public MetadataFile Module { get; }
+
+ /// The assembly reference in that names the target.
+ public AssemblyReference Reference { get; }
+
+ /// True when the reference asks for a different version than the target defines.
+ public bool IsVersionMismatch { get; }
+ }
+
+ ///
+ /// Finds the assemblies of a list that reference a given assembly (the reverse direction of the
+ /// References folder). A reference matches by simple name and public key token; the version may
+ /// differ and is reported through .
+ ///
+ public static class ReferencedByFinder
+ {
+ ///
+ /// Returns every assembly in other than
+ /// itself that has an assembly reference matching , ordered by
+ /// short name. Assemblies that failed to load are skipped.
+ ///
+ public static IReadOnlyList FindReferencingAssemblies(
+ IEnumerable candidates, MetadataFile target, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(candidates);
+ ArgumentNullException.ThrowIfNull(target);
+ var results = new List();
+ if (!target.IsAssembly)
+ return results;
+ foreach (var candidate in candidates)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var module = candidate.GetMetadataFileOrNull();
+ if (module == null || ReferenceEquals(module, target) || !module.IsAssembly)
+ continue;
+ if (string.Equals(module.FileName, target.FileName, StringComparison.OrdinalIgnoreCase))
+ continue;
+ foreach (var reference in module.AssemblyReferences)
+ {
+ if (reference.IsReferenceTo(target.Metadata))
+ {
+ results.Add(new ReferencingAssembly(candidate, module, reference,
+ reference.IsVersionMismatch(target.Metadata)));
+ break;
+ }
+ }
+ }
+ return results
+ .OrderBy(r => r.Assembly.ShortName, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(r => r.Assembly.FileName, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+ }
+}
diff --git a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs
index 2ec2be441f9..a8567ac8b07 100644
--- a/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs
+++ b/ILSpy.Tests/Analyzers/AnalyzeContextMenuTests.cs
@@ -58,10 +58,11 @@ public async Task Analyze_Entry_Is_Registered_With_The_Localised_Header()
}
[AvaloniaTest]
- public async Task Analyze_Entry_Is_Visible_For_Member_Tree_Nodes_And_Hidden_For_AssemblyTreeNode()
+ public async Task Analyze_Entry_Is_Visible_For_Member_And_Assembly_Tree_Nodes()
{
// AnalyzeContextMenuEntry visibility contract: visible when every selected node is
- // an IMemberTreeNode (types, methods, fields, properties, events), hidden otherwise.
+ // an IMemberTreeNode (types, methods, fields, properties, events) or an assembly
+ // (analyzed as a whole module), hidden otherwise.
var (_, vm) = await TestHarness.BootAsync();
@@ -75,7 +76,7 @@ public async Task Analyze_Entry_Is_Visible_For_Member_Tree_Nodes_And_Hidden_For_
var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq");
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new[] { (SharpTreeNode)assemblyNode } })
- .Should().BeFalse("AssemblyTreeNode isn't an IMemberTreeNode — Analyze must hide");
+ .Should().BeTrue("an assembly is analyzed as a module (Referenced By, Dependent Code)");
entry.IsVisible(new TextViewContext { SelectedTreeNodes = null })
.Should().BeFalse("no selection means no entity to analyse");
@@ -149,7 +150,12 @@ public async Task Analyze_Command_CanExecute_Tracks_The_Assembly_Tree_Selection(
var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq");
vm.AssemblyTreeModel.SelectNode(assemblyNode);
- command.CanExecute(null).Should().BeFalse("assembly nodes are not analyzable members");
+ command.CanExecute(null).Should().BeTrue("assembly nodes are analyzed as whole modules");
+
+ var referencesFolder = assemblyNode.Children.First(c => c.GetType().Name == "ReferenceFolderTreeNode");
+ vm.AssemblyTreeModel.SelectNode(referencesFolder);
+
+ command.CanExecute(null).Should().BeFalse("a references folder is neither a member nor an assembly");
}
[AvaloniaTest]
diff --git a/ILSpy.Tests/Dependencies/DependencyFixtures.cs b/ILSpy.Tests/Dependencies/DependencyFixtures.cs
new file mode 100644
index 00000000000..b6bd6a1ca16
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/DependencyFixtures.cs
@@ -0,0 +1,215 @@
+// 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), Model, LibAttribute and the enum Mode; the consumer
+/// uses them in a method body (), in a signature
+/// (), as a base type (), as an attribute
+/// on a parameter () and inside the arguments of its own
+/// attribute (, ,
+/// ), and has one type that does not touch the library at all
+/// ().
+///
+static class DependencyFixtures
+{
+ public const string BodyUserType = "UsesInBody";
+ public const string BodyUserMethod = "Run";
+ public const string SignatureUserType = "UsesInSignature";
+ public const string SignatureUserMethod = "Echo";
+ public const string DerivedType = "DerivedModel";
+ public const string IndependentType = "Independent";
+ public const string IndependentMethod = "Twice";
+ public const string ParameterAttributeUserType = "UsesOnParameter";
+ public const string ParameterAttributeUserMethod = "Take";
+ public const string TypeofArgumentUserType = "UsesInTypeofArgument";
+ public const string BoxedEnumArgumentUserType = "UsesInBoxedArgument";
+ public const string EnumArgumentUserType = "UsesInEnumArgument";
+
+ /// 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, Version? version = null)
+ {
+ var assemblyName = new AssemblyName(name) { Version = 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 model = module.DefineType($"{name}.Model", TypeAttributes.Public | TypeAttributes.Class);
+ model.DefineDefaultConstructor(MethodAttributes.Public);
+ model.CreateType();
+
+ var attribute = module.DefineType($"{name}.LibAttribute", TypeAttributes.Public | TypeAttributes.Class, typeof(Attribute));
+ attribute.DefineDefaultConstructor(MethodAttributes.Public);
+ attribute.CreateType();
+
+ var mode = module.DefineEnum($"{name}.Mode", TypeAttributes.Public, typeof(int));
+ mode.DefineLiteral("A", 0);
+ mode.DefineLiteral("B", 1);
+ mode.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 modelType = library.GetType($"{libraryName}.Model", throwOnError: true)!;
+ var attributeType = library.GetType($"{libraryName}.LibAttribute", throwOnError: true)!;
+ var modeType = library.GetType($"{libraryName}.Mode", 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 signatureUser = module.DefineType($"{name}.{SignatureUserType}", TypeAttributes.Public | TypeAttributes.Class);
+ var echo = signatureUser.DefineMethod(SignatureUserMethod, MethodAttributes.Public | MethodAttributes.Static, modelType, [modelType]);
+ il = echo.GetILGenerator();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ret);
+ signatureUser.CreateType();
+
+ var derived = module.DefineType($"{name}.{DerivedType}", TypeAttributes.Public | TypeAttributes.Class, modelType);
+ derived.CreateType();
+
+ var independent = module.DefineType($"{name}.{IndependentType}", TypeAttributes.Public | TypeAttributes.Class);
+ var twice = independent.DefineMethod(IndependentMethod, MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int)]);
+ il = twice.GetILGenerator();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Add);
+ il.Emit(OpCodes.Ret);
+ independent.CreateType();
+
+ var parameterUser = module.DefineType($"{name}.{ParameterAttributeUserType}", TypeAttributes.Public | TypeAttributes.Class);
+ var take = parameterUser.DefineMethod(ParameterAttributeUserMethod, MethodAttributes.Public | MethodAttributes.Static, typeof(void), [typeof(int)]);
+ take.DefineParameter(1, ParameterAttributes.None, "x")
+ .SetCustomAttribute(new CustomAttributeBuilder(attributeType.GetConstructor(Type.EmptyTypes)!, []));
+ take.GetILGenerator().Emit(OpCodes.Ret);
+ parameterUser.CreateType();
+
+ // A local attribute whose constructors take a System.Type, an object or a library enum.
+ var local = module.DefineType($"{name}.LocalAttribute", TypeAttributes.Public | TypeAttributes.Class, typeof(Attribute));
+ var baseCtor = typeof(Attribute).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, Type.EmptyTypes)!;
+ ConstructorBuilder DefineLocalCtor(Type parameterType)
+ {
+ var ctor = local.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [parameterType]);
+ var ctorIL = ctor.GetILGenerator();
+ ctorIL.Emit(OpCodes.Ldarg_0);
+ ctorIL.Emit(OpCodes.Call, baseCtor);
+ ctorIL.Emit(OpCodes.Ret);
+ return ctor;
+ }
+ var typeCtor = DefineLocalCtor(typeof(Type));
+ var objectCtor = DefineLocalCtor(typeof(object));
+ var enumCtor = DefineLocalCtor(modeType);
+ local.CreateType();
+
+ void DefineAttributedType(string typeName, ConstructorInfo ctor, object argument)
+ {
+ var type = module.DefineType($"{name}.{typeName}", TypeAttributes.Public | TypeAttributes.Class);
+ type.SetCustomAttribute(new CustomAttributeBuilder(ctor, [argument]));
+ type.CreateType();
+ }
+ DefineAttributedType(TypeofArgumentUserType, typeCtor, modelType);
+ DefineAttributedType(BoxedEnumArgumentUserType, objectCtor, Enum.ToObject(modeType, 1));
+ DefineAttributedType(EnumArgumentUserType, enumCtor, Enum.ToObject(modeType, 1));
+
+ 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/DependencyUiTests.cs b/ILSpy.Tests/Dependencies/DependencyUiTests.cs
new file mode 100644
index 00000000000..0ec967b807a
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/DependencyUiTests.cs
@@ -0,0 +1,158 @@
+// 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 Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.ILSpyX.TreeView;
+
+using ICSharpCode.ILSpy.Analyzers;
+using ICSharpCode.ILSpy.Analyzers.TreeNodes;
+using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.Dependencies;
+using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TreeNodes;
+using ICSharpCode.ILSpy.ViewModels;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Dependencies;
+
+[TestFixture]
+public class DependencyUiTests
+{
+ static async Task<(MainWindowViewModel Vm, string LibraryName, string ConsumerName)> OpenPairAsync(Avalonia.Controls.Window? _ = null)
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ var (libraryPath, consumerPath, libraryName, consumerName) = DependencyFixtures.EmitPair("Ui");
+ await vm.OpenAssemblyAsync(libraryPath);
+ await vm.OpenAssemblyAsync(consumerPath);
+ return (vm, libraryName, consumerName);
+ }
+
+ static TextViewContext Select(SharpTreeNode node) => new() { SelectedTreeNodes = [node] };
+
+ [AvaloniaTest]
+ public async Task Assembly_Node_Has_A_Referenced_By_Folder_Listing_Its_Consumers()
+ {
+ var (vm, libraryName, consumerName) = await OpenPairAsync();
+ var library = vm.AssemblyTreeModel.FindNode(libraryName);
+
+ var folder = library.GetChild();
+ folder.Text.Should().Be(Resources.ReferencedBy);
+ var child = folder.GetChild();
+ child.Assembly.ShortName.Should().Be(consumerName);
+ child.IsVersionMismatch.Should().BeFalse();
+ ((string)child.Text).Should().StartWith(consumerName);
+
+ // The consumer itself has no consumers: its folder is empty.
+ var consumer = vm.AssemblyTreeModel.FindNode(consumerName);
+ var consumerFolder = consumer.GetChild();
+ consumerFolder.EnsureLazyChildren();
+ consumerFolder.Children.Should().BeEmpty();
+ }
+
+ [AvaloniaTest]
+ public async Task Analyze_Accepts_An_Assembly_And_Shows_The_Module_Analyzers()
+ {
+ var (vm, libraryName, _) = await OpenPairAsync();
+ var library = vm.AssemblyTreeModel.FindNode(libraryName);
+ var entry = AppComposition.Current.GetExport().GetEntry(nameof(Resources.Analyze));
+
+ entry.IsVisible(Select(library)).Should().BeTrue();
+ entry.IsEnabled(Select(library)).Should().BeTrue();
+ entry.Execute(Select(library));
+
+ var analyzerVm = AppComposition.Current.GetExport();
+ var moduleRow = analyzerVm.Root.Children.OfType().Single();
+ moduleRow.Module.MetadataFile.Should().BeSameAs(library.LoadedAssembly.GetMetadataFileOrNull());
+ moduleRow.EnsureLazyChildren();
+ moduleRow.Children.OfType().Select(r => r.AnalyzerHeader)
+ .Should().Contain([ReferencedByContextMenuEntry.AnalyzerHeader, DependentCodeContextMenuEntry.AnalyzerHeader]);
+
+ entry.Execute(Select(library));
+ analyzerVm.Root.Children.OfType().Should().ContainSingle("re-analyzing reuses the row");
+ }
+
+ [AvaloniaTest]
+ public async Task Referenced_By_Context_Entry_Lists_The_Consumer_In_The_Analyzer_Pane()
+ {
+ var (vm, libraryName, consumerName) = await OpenPairAsync();
+ var library = vm.AssemblyTreeModel.FindNode(libraryName);
+ var registry = AppComposition.Current.GetExport();
+ registry.Entries.Single(e => e.Metadata.Header == nameof(Resources.ReferencedBy)).Metadata.Category
+ .Should().Be("Navigation");
+ var entry = registry.GetEntry(nameof(Resources.ReferencedBy));
+
+ entry.IsVisible(Select(library)).Should().BeTrue();
+ entry.Execute(Select(library));
+
+ var analyzerVm = AppComposition.Current.GetExport();
+ var row = (AnalyzerSearchTreeNode)analyzerVm.SelectedItems.Single();
+ row.AnalyzerHeader.Should().Be(ReferencedByContextMenuEntry.AnalyzerHeader);
+ await Waiters.WaitForAsync(() => !row.IsLoading && row.Children.OfType().Any(),
+ description: "Referenced By results");
+ row.Children.OfType().Single().Module.AssemblyName.Should().Be(consumerName);
+ }
+
+ [AvaloniaTest]
+ public async Task Module_Entries_Are_Disabled_For_An_Unresolved_Assembly_Reference()
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ var (consumerPath, missingName) = DependencyFixtures.EmitConsumerWithMissingReference("Ui");
+ var consumer = await vm.OpenAssemblyAsync(consumerPath);
+ var consumerNode = vm.AssemblyTreeModel.FindNode(consumer.ShortName);
+ consumerNode.EnsureLazyChildren();
+ var folder = consumerNode.Children.OfType().Single();
+ folder.EnsureLazyChildren();
+ var reference = folder.Children.OfType()
+ .Single(r => r.AssemblyReference.Name == missingName);
+ var registry = AppComposition.Current.GetExport();
+
+ foreach (var header in new[] { nameof(Resources.ReferencedBy), nameof(Resources.DependentCode) })
+ {
+ var entry = registry.GetEntry(header);
+ entry.IsVisible(Select(reference)).Should().BeTrue();
+ entry.IsEnabled(Select(reference)).Should().BeFalse($"{header} has no module to analyze");
+ }
+ }
+
+ [AvaloniaTest]
+ public async Task Dependent_Code_Context_Entry_Lists_The_Using_Members()
+ {
+ var (vm, libraryName, consumerName) = await OpenPairAsync();
+ var library = vm.AssemblyTreeModel.FindNode(libraryName);
+ var entry = AppComposition.Current.GetExport().GetEntry(nameof(Resources.DependentCode));
+
+ entry.IsVisible(Select(library)).Should().BeTrue();
+ entry.Execute(Select(library));
+
+ var analyzerVm = AppComposition.Current.GetExport();
+ var row = (AnalyzerSearchTreeNode)analyzerVm.SelectedItems.Single();
+ row.AnalyzerHeader.Should().Be(DependentCodeContextMenuEntry.AnalyzerHeader);
+ await Waiters.WaitForAsync(() => !row.IsLoading && row.Children.OfType().Any(),
+ description: "Dependent Code results");
+ var members = row.Children.OfType().Select(n => n.Member?.FullName).ToList();
+ members.Should().Contain($"{consumerName}.{DependencyFixtures.BodyUserType}.{DependencyFixtures.BodyUserMethod}");
+ members.Should().NotContain($"{consumerName}.{DependencyFixtures.IndependentType}.{DependencyFixtures.IndependentMethod}");
+ }
+}
diff --git a/ILSpy.Tests/Dependencies/ModuleAnalyzersTests.cs b/ILSpy.Tests/Dependencies/ModuleAnalyzersTests.cs
new file mode 100644
index 00000000000..3b6df721876
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/ModuleAnalyzersTests.cs
@@ -0,0 +1,128 @@
+// 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;
+using System.Threading.Tasks;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpyX;
+using ICSharpCode.ILSpyX.Analyzers;
+using ICSharpCode.ILSpyX.Analyzers.Builtin;
+
+using ICSharpCode.ILSpy.Languages;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Dependencies;
+
+[TestFixture]
+public class ModuleAnalyzersTests
+{
+ AssemblyList list = null!;
+ LoadedAssembly library = null!;
+ LoadedAssembly consumer = null!;
+ string consumerName = null!;
+
+ [OneTimeSetUp]
+ public async Task Setup()
+ {
+ var (libraryPath, consumerPath, _, name) = DependencyFixtures.EmitPair("Analyze");
+ consumerName = name;
+ list = new AssemblyList();
+ library = list.OpenAssembly(libraryPath);
+ consumer = list.OpenAssembly(consumerPath);
+ await library.GetLoadResultAsync();
+ await consumer.GetLoadResultAsync();
+ // An assembly that has nothing to do with the library must not contribute results.
+ await list.OpenAssembly(FixtureAssembly.Emit(DependencyFixtures.UniqueName("Bystander"))).GetLoadResultAsync();
+ }
+
+ AnalyzerContext CreateContext(CancellationToken ct = default)
+ => new() { AssemblyList = list, Language = new CSharpLanguage(), CancellationToken = ct };
+
+ IModule LibraryModule()
+ => new DecompilerTypeSystem(library.GetMetadataFileOrNull()!, library.GetAssemblyResolver()).MainModule;
+
+ [Test]
+ public void Module_Analyzers_Show_Only_For_Assemblies()
+ {
+ var module = LibraryModule();
+ var type = module.TopLevelTypeDefinitions.First(t => t.Name == "Api");
+
+ new ModuleDependentCodeAnalyzer().Show(module).Should().BeTrue();
+ new ModuleDependentCodeAnalyzer().Show(type).Should().BeFalse();
+ new ModuleDependentCodeAnalyzer().Show(null).Should().BeFalse();
+ new ModuleReferencedByAnalyzer().Show(module).Should().BeTrue();
+ new ModuleReferencedByAnalyzer().Show(type).Should().BeFalse();
+ }
+
+ [Test]
+ public void Dependent_Code_Lists_Body_Signature_And_Base_Type_Users_Only()
+ {
+ var results = new ModuleDependentCodeAnalyzer().Analyze(LibraryModule(), CreateContext()).ToList();
+
+ var names = results.OfType().Select(e => e.FullName).ToList();
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.BodyUserType}.{DependencyFixtures.BodyUserMethod}");
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.SignatureUserType}.{DependencyFixtures.SignatureUserMethod}");
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.DerivedType}");
+ names.Should().NotContain(n => n.Contains(DependencyFixtures.IndependentType, StringComparison.Ordinal));
+ results.OfType().Should().OnlyContain(e => e.ParentModule!.MetadataFile == consumer.GetMetadataFileOrNull(),
+ "only assemblies that reference the library contribute");
+ names.Should().OnlyHaveUniqueItems();
+ }
+
+ [Test]
+ public void Dependent_Code_Lists_Attribute_Users_Through_Parameters_And_Arguments()
+ {
+ var names = new ModuleDependentCodeAnalyzer().Analyze(LibraryModule(), CreateContext())
+ .OfType().Select(e => e.FullName).ToList();
+
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.ParameterAttributeUserType}.{DependencyFixtures.ParameterAttributeUserMethod}",
+ "a library attribute on a parameter is reported as its method");
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.TypeofArgumentUserType}",
+ "typeof(library type) in an attribute argument is a dependency");
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.BoxedEnumArgumentUserType}",
+ "a boxed library enum in an attribute argument is a dependency");
+ names.Should().Contain($"{consumerName}.{DependencyFixtures.EnumArgumentUserType}",
+ "a library enum in an attribute argument is a dependency");
+ }
+
+ [Test]
+ public void Dependent_Code_Honours_Cancellation()
+ {
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ var act = () => new ModuleDependentCodeAnalyzer().Analyze(LibraryModule(), CreateContext(cts.Token)).ToList();
+
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Referenced_By_Lists_The_Consumer_Module()
+ {
+ var results = new ModuleReferencedByAnalyzer().Analyze(LibraryModule(), CreateContext()).ToList();
+
+ results.Should().ContainSingle().Which.Should().BeAssignableTo()
+ .Which.MetadataFile.Should().BeSameAs(consumer.GetMetadataFileOrNull());
+ }
+}
diff --git a/ILSpy.Tests/Dependencies/ReferencedByFinderTests.cs b/ILSpy.Tests/Dependencies/ReferencedByFinderTests.cs
new file mode 100644
index 00000000000..6e6353782c6
--- /dev/null
+++ b/ILSpy.Tests/Dependencies/ReferencedByFinderTests.cs
@@ -0,0 +1,101 @@
+// 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.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 ReferencedByFinderTests
+{
+ static async Task OpenAsync(AssemblyList list, string path)
+ {
+ var loaded = list.OpenAssembly(path);
+ await loaded.GetLoadResultAsync();
+ return loaded;
+ }
+
+ [Test]
+ public async Task Finds_The_Consumer_Of_A_Library_And_Nothing_Else()
+ {
+ var (libraryPath, consumerPath, _, _) = DependencyFixtures.EmitPair("RefBy");
+ var other = FixtureAssembly.Emit(DependencyFixtures.UniqueName("Unrelated"));
+ var list = new AssemblyList();
+ var library = await OpenAsync(list, libraryPath);
+ var consumer = await OpenAsync(list, consumerPath);
+ await OpenAsync(list, other);
+
+ var result = ReferencedByFinder.FindReferencingAssemblies(list.GetAssemblies(), library.GetMetadataFileOrNull()!);
+
+ result.Should().ContainSingle();
+ result[0].Assembly.Should().BeSameAs(consumer);
+ result[0].Reference.Name.Should().Be(library.ShortName);
+ result[0].IsVersionMismatch.Should().BeFalse();
+ }
+
+ [Test]
+ public async Task A_Different_Referenced_Version_Still_Matches_And_Is_Reported()
+ {
+ var dir = DependencyFixtures.NewDirectory();
+ var libraryName = DependencyFixtures.UniqueName("RefByLib");
+ var libraryPath = DependencyFixtures.EmitLibrary(dir, libraryName, new Version(1, 0, 0, 0));
+ var consumerPath = DependencyFixtures.EmitConsumer(dir, DependencyFixtures.UniqueName("RefByApp"), libraryPath);
+ DependencyFixtures.EmitLibrary(dir, libraryName, new Version(3, 1, 0, 0));
+ var list = new AssemblyList();
+ var library = await OpenAsync(list, libraryPath);
+ await OpenAsync(list, consumerPath);
+
+ var result = ReferencedByFinder.FindReferencingAssemblies(list.GetAssemblies(), library.GetMetadataFileOrNull()!);
+
+ result.Should().ContainSingle().Which.IsVersionMismatch.Should().BeTrue();
+ }
+
+ [Test]
+ public void Matching_Requires_The_Same_Culture_And_Public_Key_Token()
+ {
+ // System.Runtime is strong-named; a reference with the right name but no token is a different assembly.
+ var runtimePath = Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location)!, "System.Runtime.dll");
+ using var runtime = new PEFile(runtimePath);
+ string token = runtime.Metadata.GetPublicKeyToken();
+
+ token.Should().NotBe("null");
+ AssemblyNameReference.Parse($"System.Runtime, Version=1.0.0.0, Culture=neutral, PublicKeyToken={token}")
+ .IsReferenceTo(runtime.Metadata)
+ .Should().BeTrue("name and token match; the version is not part of the identity");
+ AssemblyNameReference.Parse("System.Runtime, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
+ .IsReferenceTo(runtime.Metadata)
+ .Should().BeFalse();
+ AssemblyNameReference.Parse($"System.Runtime2, Version=1.0.0.0, Culture=neutral, PublicKeyToken={token}")
+ .IsReferenceTo(runtime.Metadata)
+ .Should().BeFalse();
+ AssemblyNameReference.Parse($"System.Runtime, Version=1.0.0.0, Culture=de-DE, PublicKeyToken={token}")
+ .IsReferenceTo(runtime.Metadata)
+ .Should().BeFalse("the culture is part of the assembly identity");
+ }
+}
diff --git a/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs b/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs
index 49861418d40..c01e31e3065 100644
--- a/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs
+++ b/ILSpy/Analyzers/AnalyzeContextMenuEntry.cs
@@ -69,8 +69,9 @@ public static bool IsVisibleForContext(TextViewContext context)
if (context.SelectedTreeNodes is { Length: > 0 } nodes)
{
// Top-level analyzer rows are already analysed (Remove is the entry for those);
- // result rows underneath promote their entity to a new top-level row.
- return nodes.All(n => n is IMemberTreeNode
+ // result rows underneath promote their entity to a new top-level row. Assembly
+ // nodes analyse the whole module.
+ return nodes.All(n => (n is IMemberTreeNode || n is AssemblyTreeNode)
&& n is not AnalyzerEntityTreeNode { Parent.IsRoot: true });
}
// Right-clicking a resolved symbol in the decompiled code: the reference carries the entity.
@@ -80,7 +81,7 @@ public static bool IsVisibleForContext(TextViewContext context)
public static bool IsEnabledForContext(TextViewContext context)
{
if (context.SelectedTreeNodes is { Length: > 0 } nodes)
- return nodes.OfType().All(n => IsAnalysable(n.Member));
+ return nodes.All(n => SymbolOf(n) is { } symbol && IsAnalysable(symbol));
return context.Reference?.Reference is IEntity entity && IsAnalysable(entity);
}
@@ -100,28 +101,44 @@ public static bool Analyze(TextViewContext context, AnalyzerTreeViewModel analyz
return false;
}
- // The analysable entities for this invocation: a tree-node selection (assembly/analyzer tree),
+ // The analysable symbols for this invocation: a tree-node selection (assembly/analyzer tree),
// or the single resolved entity under a right-clicked code reference (decompiler text view).
- static System.Collections.Generic.List MembersToAnalyse(TextViewContext context)
+ static System.Collections.Generic.List MembersToAnalyse(TextViewContext context)
{
if (context.SelectedTreeNodes is { Length: > 0 } nodes)
{
- return nodes.OfType()
- .Select(n => n.Member)
+ return nodes.Select(SymbolOf)
.Where(IsAnalysable)
.Select(m => m!)
.ToList();
}
if (context.Reference?.Reference is IEntity entity && IsAnalysable(entity))
- return new System.Collections.Generic.List { entity };
- return new System.Collections.Generic.List();
+ return new System.Collections.Generic.List { entity };
+ return new System.Collections.Generic.List();
}
+ ///
+ /// The symbol a selected node stands for: the member of a member row, the module of an
+ /// assembly node or of a module row in the analyzer pane.
+ ///
+ internal static ISymbol? SymbolOf(ICSharpCode.ILSpyX.TreeView.SharpTreeNode node) => node switch {
+ ICSharpCode.ILSpy.Analyzers.TreeNodes.AnalyzedModuleTreeNode moduleNode => moduleNode.Module,
+ IMemberTreeNode memberNode => memberNode.Member,
+ AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true } assemblyNode
+ => assemblyNode.LoadedAssembly.GetTypeSystemOrNull()?.MainModule,
+ _ => null,
+ };
+
///
/// Const fields are textual literals at every use-site rather than entities the
- /// analyser can match against — exclude them so the entry stays disabled.
+ /// analyser can match against, so they are excluded and the entry stays disabled. Modules
+ /// are analysable when they carry metadata.
///
- static bool IsAnalysable(IEntity? entity) => entity is not null and not IField { IsConst: true };
+ static bool IsAnalysable(ISymbol? symbol) => symbol switch {
+ IModule module => module.MetadataFile != null,
+ IEntity entity => entity is not IField { IsConst: true },
+ _ => false,
+ };
}
[Export]
diff --git a/ILSpy/Analyzers/AnalyzerTreeViewModel.cs b/ILSpy/Analyzers/AnalyzerTreeViewModel.cs
index cc796902bcc..05745b8f98a 100644
--- a/ILSpy/Analyzers/AnalyzerTreeViewModel.cs
+++ b/ILSpy/Analyzers/AnalyzerTreeViewModel.cs
@@ -84,6 +84,41 @@ public AnalyzerEntityTreeNode Analyze(IEntity entity)
return node;
}
+ ///
+ /// Adds a whole module (assembly) to the analyzer pane, reusing the existing row for the same
+ /// loaded file, and selects it. Its children are the analyzers that apply to modules
+ /// ("Referenced By", "Dependent Code", ...).
+ ///
+ public AnalyzerEntityTreeNode Analyze(IModule module)
+ {
+ ArgumentNullException.ThrowIfNull(module);
+ var existing = Root.Children
+ .OfType()
+ .FirstOrDefault(n => n.Module.MetadataFile != null
+ && ReferenceEquals(n.Module.MetadataFile, module.MetadataFile));
+ if (existing != null)
+ {
+ SyncSelection(existing);
+ return existing;
+ }
+ var node = new AnalyzedModuleTreeNode(module, source: null);
+ Root.Children.Add(node);
+ node.IsExpanded = true;
+ SyncSelection(node);
+ return node;
+ }
+
+ /// Dispatches to or .
+ public AnalyzerEntityTreeNode Analyze(ISymbol symbol)
+ {
+ return symbol switch {
+ IEntity entity => Analyze(entity),
+ IModule module => Analyze(module),
+ _ => throw new ArgumentOutOfRangeException(nameof(symbol),
+ $"Symbol {symbol?.GetType().FullName} is not supported by the analyzer pane.")
+ };
+ }
+
static bool IsSameEntity(IEntity? a, IEntity b)
{
// Entities reaching the pane come from different type systems (the assembly tree's,
diff --git a/ILSpy/Dependencies/DependencyCommands.cs b/ILSpy/Dependencies/DependencyCommands.cs
new file mode 100644
index 00000000000..6fc4a50fbf5
--- /dev/null
+++ b/ILSpy/Dependencies/DependencyCommands.cs
@@ -0,0 +1,130 @@
+// 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 System.Linq;
+
+using ICSharpCode.Decompiler.TypeSystem;
+
+using ICSharpCode.ILSpy.Analyzers;
+using ICSharpCode.ILSpy.Docking;
+using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TreeNodes;
+
+namespace ICSharpCode.ILSpy.Dependencies
+{
+ /// Shared plumbing of the dependency context-menu entries.
+ static class DependencyMenuHelpers
+ {
+ /// Context-menu category of the dependency entries.
+ public const string NavigationCategory = "Navigation";
+
+ ///
+ /// The module a single selected node stands for: an assembly node's own module, or the
+ /// assembly an assembly-reference node resolves to. Null when there is none.
+ ///
+ public static IModule? GetModule(TextViewContext context)
+ {
+ if (context.SelectedTreeNodes is not [var node])
+ return null;
+ switch (node)
+ {
+ case AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true } assemblyNode:
+ return assemblyNode.LoadedAssembly.GetTypeSystemOrNull()?.MainModule;
+ case AssemblyReferenceTreeNode referenceNode:
+ var owner = referenceNode.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (owner == null)
+ return null;
+ var resolved = owner.LoadedAssembly.GetAssemblyResolver().Resolve(referenceNode.AssemblyReference);
+ if (resolved == null)
+ return null;
+ var loaded = owner.LoadedAssembly.AssemblyList.FindAssembly(resolved.FileName);
+ return loaded?.GetTypeSystemOrNull()?.MainModule;
+ default:
+ return null;
+ }
+ }
+
+ public static bool IsModuleSelection(TextViewContext context)
+ => context.SelectedTreeNodes is [AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }]
+ or [AssemblyReferenceTreeNode];
+
+ ///
+ /// Adds to the analyzer pane, expands the analyzer row with the
+ /// given header, selects it and brings the pane to the front.
+ ///
+ public static AnalyzerSearchTreeNode? AnalyzeWith(IModule module, string analyzerHeader,
+ AnalyzerTreeViewModel analyzerTreeViewModel, DockWorkspace dockWorkspace)
+ {
+ var moduleNode = analyzerTreeViewModel.Analyze(module);
+ moduleNode.EnsureLazyChildren();
+ var row = moduleNode.Children.OfType()
+ .FirstOrDefault(r => r.AnalyzerHeader == analyzerHeader);
+ if (row != null)
+ {
+ row.IsExpanded = true;
+ analyzerTreeViewModel.SelectedItems.Clear();
+ analyzerTreeViewModel.SelectedItems.Add(row);
+ }
+ dockWorkspace.ShowToolPane(AnalyzerTreeViewModel.PaneContentId);
+ return row;
+ }
+ }
+
+ /// Right-click an assembly (or an assembly reference) -> "Referenced By" in the analyzer pane.
+ [ExportContextMenuEntry(Header = nameof(Resources.ReferencedBy), Category = DependencyMenuHelpers.NavigationCategory, Order = 600)]
+ [Shared]
+ [method: ImportingConstructor]
+ public sealed class ReferencedByContextMenuEntry(AnalyzerTreeViewModel analyzerTreeViewModel, DockWorkspace dockWorkspace) : IContextMenuEntry
+ {
+ /// Header of the module analyzer this entry opens.
+ public const string AnalyzerHeader = "Referenced By";
+
+ public bool IsVisible(TextViewContext context) => DependencyMenuHelpers.IsModuleSelection(context);
+
+ // An assembly reference that does not resolve to a loaded assembly has nothing to analyze.
+ public bool IsEnabled(TextViewContext context) => DependencyMenuHelpers.GetModule(context) != null;
+
+ public void Execute(TextViewContext context)
+ {
+ if (DependencyMenuHelpers.GetModule(context) is { } module)
+ DependencyMenuHelpers.AnalyzeWith(module, AnalyzerHeader, analyzerTreeViewModel, dockWorkspace);
+ }
+ }
+
+ /// Right-click an assembly (or an assembly reference) -> "Dependent Code" in the analyzer pane.
+ [ExportContextMenuEntry(Header = nameof(Resources.DependentCode), Category = DependencyMenuHelpers.NavigationCategory, Order = 610)]
+ [Shared]
+ [method: ImportingConstructor]
+ public sealed class DependentCodeContextMenuEntry(AnalyzerTreeViewModel analyzerTreeViewModel, DockWorkspace dockWorkspace) : IContextMenuEntry
+ {
+ /// Header of the module analyzer this entry opens.
+ public const string AnalyzerHeader = "Dependent Code";
+
+ public bool IsVisible(TextViewContext context) => DependencyMenuHelpers.IsModuleSelection(context);
+
+ // An assembly reference that does not resolve to a loaded assembly has nothing to analyze.
+ public bool IsEnabled(TextViewContext context) => DependencyMenuHelpers.GetModule(context) != null;
+
+ public void Execute(TextViewContext context)
+ {
+ if (DependencyMenuHelpers.GetModule(context) is { } module)
+ DependencyMenuHelpers.AnalyzeWith(module, AnalyzerHeader, analyzerTreeViewModel, dockWorkspace);
+ }
+ }
+}
diff --git a/ILSpy/Dependencies/ReferencedByTreeNodes.cs b/ILSpy/Dependencies/ReferencedByTreeNodes.cs
new file mode 100644
index 00000000000..1320a815edd
--- /dev/null
+++ b/ILSpy/Dependencies/ReferencedByTreeNodes.cs
@@ -0,0 +1,173 @@
+// 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 ICSharpCode.Decompiler;
+using ICSharpCode.Decompiler.IL;
+using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.ILSpyX;
+using ICSharpCode.ILSpyX.Dependencies;
+using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
+
+using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.AssemblyTree;
+using ICSharpCode.ILSpy.Languages;
+using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TreeNodes;
+
+namespace ICSharpCode.ILSpy.Dependencies
+{
+ ///
+ /// "Referenced By" folder under an assembly node: the reverse of the References folder. Lists
+ /// the assemblies of the current list that reference this assembly (matched by name and public
+ /// key token), each of which expands to its own referencing assemblies.
+ ///
+ public sealed class ReferencedByFolderTreeNode : ILSpyTreeNode
+ {
+ readonly MetadataFile module;
+ readonly AssemblyTreeNode parentAssembly;
+
+ public ReferencedByFolderTreeNode(MetadataFile module, AssemblyTreeNode parentAssembly)
+ {
+ this.module = module ?? throw new ArgumentNullException(nameof(module));
+ this.parentAssembly = parentAssembly ?? throw new ArgumentNullException(nameof(parentAssembly));
+ LazyLoading = true;
+ }
+
+ public MetadataFile Module => module;
+
+ public override object Text => Resources.ReferencedBy;
+
+ public override object? NavigationText => $"{Text} ({module.Name})";
+
+ public override object Icon => Images.ReferenceFolder;
+
+ protected override void LoadChildren()
+ {
+ foreach (var child in ReferencingAssemblyTreeNode.CreateChildren(parentAssembly.LoadedAssembly.AssemblyList, module, this))
+ Children.Add(child);
+ }
+
+ public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
+ {
+ EnsureLazyChildren();
+ output.WriteLine($"Assemblies in the list that reference {module.FullName}:");
+ output.WriteLine();
+ if (Children.Count == 0)
+ output.WriteLine("(none)");
+ foreach (var node in Children.OfType())
+ node.WriteSummary(output);
+ }
+ }
+
+ ///
+ /// One referencing assembly under a (or, recursively,
+ /// under another referencing assembly). Activating it selects that assembly in the tree.
+ ///
+ public sealed class ReferencingAssemblyTreeNode : ILSpyTreeNode
+ {
+ readonly ReferencingAssembly referencing;
+
+ ReferencingAssemblyTreeNode(ReferencingAssembly referencing)
+ {
+ this.referencing = referencing;
+ LazyLoading = true;
+ }
+
+ public LoadedAssembly Assembly => referencing.Assembly;
+
+ public AssemblyReference Reference => referencing.Reference;
+
+ public bool IsVersionMismatch => referencing.IsVersionMismatch;
+
+ public override object Text {
+ get {
+ var name = ILAmbience.EscapeName(referencing.Assembly.ShortName);
+ var metadata = referencing.Module.Metadata;
+ var version = metadata.IsAssembly ? metadata.GetAssemblyDefinition().Version : null;
+ var text = version != null ? $"{name} ({version})" : name;
+ if (referencing.IsVersionMismatch)
+ text += $" - references v{referencing.Reference.Version}";
+ return text;
+ }
+ }
+
+ public override object? NavigationText => $"{Text} ({Resources.ReferencedBy})";
+
+ public override object Icon => referencing.IsVersionMismatch ? Images.AssemblyWarning : Images.Assembly;
+
+ public override object? ToolTip => referencing.Assembly.FileName;
+
+ ///
+ /// One node per assembly of that references ,
+ /// skipping assemblies already on the path from up to the folder so
+ /// that reference cycles do not recurse forever.
+ ///
+ internal static System.Collections.Generic.IEnumerable CreateChildren(
+ AssemblyList list, MetadataFile target, ILSpyTreeNode parent)
+ {
+ var onPath = parent.AncestorsAndSelf()
+ .Select(n => n switch {
+ ReferencingAssemblyTreeNode r => r.referencing.Module,
+ ReferencedByFolderTreeNode f => f.Module,
+ _ => null,
+ })
+ .Where(m => m != null)
+ .ToHashSet();
+ foreach (var referencing in ReferencedByFinder.FindReferencingAssemblies(list.GetAssemblies(), target))
+ {
+ if (!onPath.Contains(referencing.Module))
+ yield return new ReferencingAssemblyTreeNode(referencing);
+ }
+ }
+
+ protected override void LoadChildren()
+ {
+ foreach (var child in CreateChildren(referencing.Assembly.AssemblyList, referencing.Module, this))
+ Children.Add(child);
+ }
+
+ public override void ActivateItem(IPlatformRoutedEventArgs e)
+ {
+ var model = AppComposition.TryGetExport();
+ if (model?.Root is not AssemblyListTreeNode listNode)
+ return;
+ var node = listNode.FindAssemblyNode(referencing.Assembly);
+ if (node == null)
+ return;
+ model.SelectedItem = node;
+ e.Handled = true;
+ }
+
+ internal void WriteSummary(ITextOutput output)
+ {
+ output.WriteLine($"{Text}");
+ output.Indent();
+ output.WriteLine("File: " + referencing.Assembly.FileName);
+ output.WriteLine("Reference: " + referencing.Reference.FullName);
+ output.Unindent();
+ }
+
+ public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
+ {
+ WriteSummary(output);
+ }
+ }
+}
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index a276ef2f82b..d6d2c027a41 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -1938,6 +1938,15 @@ public static string Dependencies {
}
}
+ ///
+ /// Looks up a localized string similar to Dependent Code.
+ ///
+ public static string DependentCode {
+ get {
+ return ResourceManager.GetString("DependentCode", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Derived Types.
///
@@ -2915,6 +2924,15 @@ public static string R_ename {
}
}
+ ///
+ /// Looks up a localized string similar to Referenced By.
+ ///
+ public static string ReferencedBy {
+ get {
+ return ResourceManager.GetString("ReferencedBy", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Referenced Types.
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 94e510454b3..cc0059520a9 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -675,6 +675,9 @@ Are you sure you want to continue?
Dependencies
+
+ Dependent Code
+
Derived Types
@@ -1010,6 +1013,9 @@ If this does not solve the problem and your system supports long paths, you can
Reference Name
+
+ Referenced By
+
Referenced Types
diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs
index f2e8bc57607..4fd383f7c34 100644
--- a/ILSpy/TreeNodes/AssemblyTreeNode.cs
+++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs
@@ -456,6 +456,8 @@ protected override void LoadChildren()
}
Children.Add(new ReferenceFolderTreeNode(module, this));
+ if (module.IsAssembly)
+ Children.Add(new ICSharpCode.ILSpy.Dependencies.ReferencedByFolderTreeNode(module, this));
if (module.Resources.Any())
Children.Add(new ResourceListTreeNode(module));