Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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));
}

Expand Down
2 changes: 1 addition & 1 deletion ArchUnitNET/Loader/InstructionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ [NotNull] this Instruction methodCallInstruction
&& methodCallAssignment.Operand is FieldReference fieldReference
)
{
return fieldReference.Resolve();
return fieldReference.TryResolve();
}

return null;
Expand Down
5 changes: 3 additions & 2 deletions ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
85 changes: 85 additions & 0 deletions ArchUnitNET/Loader/MonoCecilResolveExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using JetBrains.Annotations;
using Mono.Cecil;

namespace ArchUnitNET.Loader
{
/// <summary>
/// Resolve extensions that return <c>null</c> instead of throwing
/// <see cref="AssemblyResolutionException" /> when an assembly cannot be located.
/// </summary>
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;
}
}
}
}
9 changes: 7 additions & 2 deletions ArchUnitNET/Loader/MonoCecilTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 4 additions & 12 deletions ArchUnitNET/Loader/TypeProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -1083,7 +1075,7 @@ private static IEnumerable<TypeReference> GetInterfacesImplementedByClass(
TypeDefinition typeDefinition
)
{
var baseType = typeDefinition.BaseType?.Resolve();
var baseType = typeDefinition.BaseType.TryResolve();
var baseInterfaces =
baseType != null
? GetInterfacesImplementedByClass(baseType)
Expand Down
72 changes: 72 additions & 0 deletions ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Tests that the loader handles assemblies whose dependencies cannot be resolved
/// (e.g. when a referenced assembly is not present in the output directory).
/// </summary>
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();

/// <summary>
/// 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.
/// </summary>
[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();
}
}
}
}
Loading