diff --git a/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs b/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs index a4e798d8b..ee9e0eb2a 100644 --- a/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs +++ b/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs @@ -34,25 +34,42 @@ public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters p throw new ArgumentNullException(nameof(name)); } - if ( - _libraries.TryGetValue(name.FullName, out var assemblyDefinition) - || string.IsNullOrEmpty(AssemblyPath) - ) + if (_libraries.TryGetValue(name.FullName, out var assemblyDefinition)) { return assemblyDefinition; } - var file = Directory - .EnumerateFiles(AssemblyPath, $"{name.Name}.dll", SearchOption.AllDirectories) - .FirstOrDefault(); + if (!string.IsNullOrEmpty(AssemblyPath)) + { + var file = Directory + .EnumerateFiles(AssemblyPath, $"{name.Name}.dll", SearchOption.AllDirectories) + .FirstOrDefault(); + + if (file != null) + { + assemblyDefinition = AssemblyDefinition.ReadAssembly(file, parameters); + _libraries.Add(name.FullName, assemblyDefinition); + return assemblyDefinition; + } + } - if (file == null) + // Fall back to DefaultAssemblyResolver for framework assemblies not found in + // AssemblyPath. Pass the original parameters so the loaded assembly keeps using + // this resolver (which returns null for unresolvable references) instead of the + // DefaultAssemblyResolver (which throws). + try + { + assemblyDefinition = _defaultAssemblyResolver.Resolve(name, parameters); + } + catch (AssemblyResolutionException) { return null; } - assemblyDefinition = AssemblyDefinition.ReadAssembly(file, parameters); - _libraries.Add(name.FullName, assemblyDefinition); + if (assemblyDefinition != null) + { + _libraries.Add(name.FullName, assemblyDefinition); + } return assemblyDefinition; } @@ -84,7 +101,7 @@ [NotNull] AssemblyDefinition moduleAssembly public void AddLib(AssemblyNameReference name) { - var assembly = Resolve(name) ?? _defaultAssemblyResolver.Resolve(name); + var assembly = Resolve(name); AddLib(name, assembly ?? throw new AssemblyResolutionException(name)); } diff --git a/ArchUnitNET/Loader/InstructionExtensions.cs b/ArchUnitNET/Loader/InstructionExtensions.cs index 3df361f31..bc413a3d6 100644 --- a/ArchUnitNET/Loader/InstructionExtensions.cs +++ b/ArchUnitNET/Loader/InstructionExtensions.cs @@ -46,7 +46,7 @@ [NotNull] this Instruction methodCallInstruction && methodCallAssignment.Operand is FieldReference fieldReference ) { - return fieldReference.Resolve(); + return fieldReference.TryResolve(); } return null; diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs index 061dc5a28..834445005 100644 --- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs @@ -295,8 +295,9 @@ internal static bool IsCompilerGenerated(this MemberReference memberReference) { return true; } - var declaringType = - memberReference.Resolve()?.DeclaringType ?? memberReference.DeclaringType; + + var resolvedType = memberReference.TryResolve(); + var declaringType = resolvedType?.DeclaringType ?? memberReference.DeclaringType; return declaringType != null && declaringType.Name.HasCompilerGeneratedName(); } diff --git a/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs b/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs new file mode 100644 index 000000000..d8b25ea59 --- /dev/null +++ b/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs @@ -0,0 +1,85 @@ +using System; +using JetBrains.Annotations; +using Mono.Cecil; + +namespace ArchUnitNET.Loader +{ + /// + /// Resolve extensions that return null instead of throwing + /// when an assembly cannot be located. + /// + internal static class MonoCecilResolveExtensions + { + [CanBeNull] + public static TypeDefinition TryResolve([CanBeNull] this TypeReference typeReference) + { + if (typeReference == null) + { + return null; + } + + try + { + return typeReference.Resolve(); + } + catch (AssemblyResolutionException) + { + return null; + } + } + + [CanBeNull] + public static MethodDefinition TryResolve([CanBeNull] this MethodReference methodReference) + { + if (methodReference == null) + { + return null; + } + + try + { + return methodReference.Resolve(); + } + catch (AssemblyResolutionException) + { + return null; + } + } + + [CanBeNull] + public static FieldDefinition TryResolve([CanBeNull] this FieldReference fieldReference) + { + if (fieldReference == null) + { + return null; + } + + try + { + return fieldReference.Resolve(); + } + catch (AssemblyResolutionException) + { + return null; + } + } + + [CanBeNull] + public static IMemberDefinition TryResolve([CanBeNull] this MemberReference memberReference) + { + if (memberReference == null) + { + return null; + } + + try + { + return memberReference.Resolve(); + } + catch (AssemblyResolutionException) + { + return null; + } + } + } +} diff --git a/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs b/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs index 83af639b3..b27d536f2 100644 --- a/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs @@ -51,8 +51,13 @@ internal static bool IsAttribute([CanBeNull] this TypeDefinition typeDefinition) { if (typeDefinition?.BaseType != null) { - return typeDefinition.BaseType.FullName == "System.Attribute" - || IsAttribute(typeDefinition.BaseType.Resolve()); + if (typeDefinition.BaseType.FullName == "System.Attribute") + { + return true; + } + + var resolvedBaseType = typeDefinition.BaseType.TryResolve(); + return resolvedBaseType != null && IsAttribute(resolvedBaseType); } return false; diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs index c765cdd8a..45c470511 100644 --- a/ArchUnitNET/Loader/TypeProcessor.cs +++ b/ArchUnitNET/Loader/TypeProcessor.cs @@ -797,15 +797,7 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc if (calledMethodReference.IsCompilerGenerated()) { - MethodDefinition calledMethodDefinition; - try - { - calledMethodDefinition = calledMethodReference.Resolve(); - } - catch (AssemblyResolutionException) - { - calledMethodDefinition = null; - } + var calledMethodDefinition = calledMethodReference.TryResolve(); if (calledMethodDefinition?.Body == null) { @@ -866,7 +858,7 @@ DomainResolver domainResolver { var compilerGeneratedGeneratorObject = methodBody .Instructions.Where(inst => inst.IsNewObjectOp()) - .Select(inst => ((MethodReference)inst.Operand).DeclaringType.Resolve()) + .Select(inst => ((MethodReference)inst.Operand).DeclaringType.TryResolve()) .FirstOrDefault(type => type != null && type.Methods.Any(method => method.Name == nameof(IEnumerator.MoveNext)) @@ -911,7 +903,7 @@ DomainResolver domainResolver { var compilerGeneratedGeneratorObject = methodBody .Instructions.Where(inst => inst.IsNewObjectOp()) - .Select(inst => ((MethodReference)inst.Operand).DeclaringType.Resolve()) + .Select(inst => ((MethodReference)inst.Operand).DeclaringType.TryResolve()) .FirstOrDefault(type => type != null && type.Methods.Any(method => @@ -1083,7 +1075,7 @@ private static IEnumerable GetInterfacesImplementedByClass( TypeDefinition typeDefinition ) { - var baseType = typeDefinition.BaseType?.Resolve(); + var baseType = typeDefinition.BaseType.TryResolve(); var baseInterfaces = baseType != null ? GetInterfacesImplementedByClass(baseType) diff --git a/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs b/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs new file mode 100644 index 000000000..2ae0fb8d3 --- /dev/null +++ b/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Xunit; +using static ArchUnitNET.Loader.MonoCecilTypeExtensions; + +namespace ArchUnitNETTests.Loader +{ + /// + /// Tests that the loader handles assemblies whose dependencies cannot be resolved + /// (e.g. when a referenced assembly is not present in the output directory). + /// + public class UnresolvableAssemblyTests + { + private static string RepoRoot => + AppDomain.CurrentDomain.BaseDirectory[ + ..AppDomain.CurrentDomain.BaseDirectory.IndexOf( + @"ArchUnitNETTests", + StringComparison.InvariantCulture + ) + ]; + + private static string FilteredDirectoryLoaderTestAssemblyPath => + Directory + .EnumerateFiles( + Path.Combine(RepoRoot, "TestAssemblies", "FilteredDirectoryLoaderTestAssembly"), + "FilteredDirectoryLoaderTestAssembly.dll", + SearchOption.AllDirectories + ) + .First(); + + /// + /// DerivedAttribute extends BaseAttribute from FilteredDirectoryUnavailableTypesAssembly, + /// which is not copied to the output directory (Private=False). IsAttribute must not + /// throw when the base type's assembly is unavailable. + /// + [Fact] + public void IsAttributeHandlesUnresolvableBaseTypeAssembly() + { + var assemblyPath = FilteredDirectoryLoaderTestAssemblyPath; + Assert.True(File.Exists(assemblyPath), $"Test assembly not found at {assemblyPath}"); + + // DefaultAssemblyResolver has no search directories, so resolving + // FilteredDirectoryUnavailableTypesAssembly will fail. + var resolver = new DefaultAssemblyResolver(); + var module = ModuleDefinition.ReadModule( + assemblyPath, + new ReaderParameters { AssemblyResolver = resolver } + ); + + try + { + var derivedAttribute = module.Types.FirstOrDefault(t => + t.FullName == "FilteredDirectoryLoaderTestAssembly.DerivedAttribute" + ); + Assert.NotNull(derivedAttribute); + Assert.Equal( + "FilteredDirectoryUnavailableTypesAssembly.BaseAttribute", + derivedAttribute.BaseType.FullName + ); + + Assert.False(derivedAttribute.IsAttribute()); + } + finally + { + module.Dispose(); + resolver.Dispose(); + } + } + } +}