From 69b7c133984f402e9f6741e861c50279aeb48c5b Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:03:19 +0100 Subject: [PATCH 01/15] chore: minor code quality improvements - Convert DotNetCoreAssemblyResolver.AssemblyPath from public field to auto-property for better encapsulation - Make FilterResult static fields readonly to prevent mutation - Optimize MethodReference.BuildFullName with early return and StringBuilder instead of string concatenation via Aggregate Signed-off-by: Alexander Linne --- .../Loader/DotNetCoreAssemblyResolver.cs | 2 +- ArchUnitNET/Loader/FilterResult.cs | 8 ++++---- ArchUnitNET/Loader/MonoCecilMemberExtensions.cs | 17 ++++++++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs b/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs index 4eec205a3..a4e798d8b 100644 --- a/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs +++ b/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs @@ -12,7 +12,7 @@ internal class DotNetCoreAssemblyResolver : IAssemblyResolver { private readonly DefaultAssemblyResolver _defaultAssemblyResolver; private readonly Dictionary _libraries; - public string AssemblyPath = ""; + public string AssemblyPath { get; set; } = ""; public DotNetCoreAssemblyResolver() { diff --git a/ArchUnitNET/Loader/FilterResult.cs b/ArchUnitNET/Loader/FilterResult.cs index cc9dbbc9a..c99bcb5fa 100644 --- a/ArchUnitNET/Loader/FilterResult.cs +++ b/ArchUnitNET/Loader/FilterResult.cs @@ -16,22 +16,22 @@ public struct FilterResult /// /// Load this assembly and traverse its dependencies /// - public static FilterResult LoadAndContinue = new FilterResult(true, true); + public static readonly FilterResult LoadAndContinue = new FilterResult(true, true); /// /// Do not load this assembly, but traverse its dependencies /// - public static FilterResult SkipAndContinue = new FilterResult(true, false); + public static readonly FilterResult SkipAndContinue = new FilterResult(true, false); /// /// Load this assembly and do not traverse its dependencies /// - public static FilterResult LoadAndStop = new FilterResult(false, true); + public static readonly FilterResult LoadAndStop = new FilterResult(false, true); /// /// Do not load this assembly and do not traverse its dependencies /// - public static FilterResult DontLoadAndStop = new FilterResult(false, false); + public static readonly FilterResult DontLoadAndStop = new FilterResult(false, false); private FilterResult(bool traverseDependencies, bool loadThisAssembly) { diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs index 20379af43..36027e32c 100644 --- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs @@ -30,11 +30,18 @@ internal static class MonoCecilMemberExtensions internal static string BuildFullName(this MethodReference methodReference) { - return methodReference.FullName - + methodReference.GenericParameters.Aggregate( - string.Empty, - (current, newElement) => current + "<" + newElement.Name + ">" - ); + if (!methodReference.HasGenericParameters) + { + return methodReference.FullName; + } + + var sb = new StringBuilder(methodReference.FullName); + foreach (var p in methodReference.GenericParameters) + { + sb.Append('<').Append(p.Name).Append('>'); + } + + return sb.ToString(); } [NotNull] From 282e14438c4b2778bb16a17dd5f9659988163e13 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:03:52 +0100 Subject: [PATCH 02/15] fix: use HashSet for assembly deduplication in ArchLoader Replace List with HashSet keyed by FullName for tracking processed assemblies. The previous approach used List.Contains with reference equality on AssemblyNameReference objects, which could fail to detect duplicates when different reference instances point to the same assembly. Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchLoader.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ArchUnitNET/Loader/ArchLoader.cs b/ArchUnitNET/Loader/ArchLoader.cs index 5ac7ab920..74b0da958 100644 --- a/ArchUnitNET/Loader/ArchLoader.cs +++ b/ArchUnitNET/Loader/ArchLoader.cs @@ -125,7 +125,7 @@ private void LoadModule( fileName, new ReaderParameters { AssemblyResolver = _assemblyResolver } ); - var processedAssemblies = new List { module.Assembly.Name }; + var processedAssemblies = new HashSet { module.Assembly.Name.FullName }; var resolvedModules = new List(); _assemblyResolver.AddLib(module.Assembly); _archBuilder.AddAssembly(module.Assembly, false); @@ -144,7 +144,7 @@ private void LoadModule( { try { - processedAssemblies.Add(assemblyReference); + processedAssemblies.Add(assemblyReference.FullName); _assemblyResolver.AddLib(assemblyReference); if (includeDependencies) { @@ -176,17 +176,17 @@ private void LoadModule( private void AddReferencedAssembliesRecursively( AssemblyNameReference currentAssemblyReference, - ICollection processedAssemblies, + ICollection processedAssemblies, List resolvedModules, FilterFunc filterFunc ) { - if (processedAssemblies.Contains(currentAssemblyReference)) + if (processedAssemblies.Contains(currentAssemblyReference.FullName)) { return; } - processedAssemblies.Add(currentAssemblyReference); + processedAssemblies.Add(currentAssemblyReference.FullName); try { _assemblyResolver.AddLib(currentAssemblyReference); From 05f626919d2efe5152350ebe797420c24092f4b2 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:04:26 +0100 Subject: [PATCH 03/15] refactor: replace RegexUtils.MatchNamespaces with inline StartsWith The RegexUtils.MatchNamespaces helper simply delegates to string.StartsWith. Inline the check directly in ArchBuilder to remove an unnecessary indirection. The remaining RegexUtils methods (MatchGetPropertyName, MatchSetPropertyName) are still in use. Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index db9af6a0d..ccdaa2f72 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -90,7 +90,10 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) var currentTypes = new List(types.Count); types .Where(typeDefinition => - RegexUtils.MatchNamespaces(namespaceFilter, typeDefinition.Namespace) + ( + namespaceFilter == null + || typeDefinition.Namespace.StartsWith(namespaceFilter) + ) && typeDefinition.CustomAttributes.All(att => att.AttributeType.FullName != "Microsoft.VisualStudio.TestPlatform.TestSDKAutoGeneratedCode" From 186706b5a3dd7bf2c34e9b42d6c13773b3c59aa8 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:04:57 +0100 Subject: [PATCH 04/15] fix: mark function pointer types as stubs Function pointer types are synthetic types created during dependency resolution, not types directly loaded from assemblies. Setting isStub=true ensures they are correctly classified as referenced types rather than primary architecture types. Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/TypeFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchUnitNET/Loader/TypeFactory.cs b/ArchUnitNET/Loader/TypeFactory.cs index 240598597..9aa7d7e82 100644 --- a/ArchUnitNET/Loader/TypeFactory.cs +++ b/ArchUnitNET/Loader/TypeFactory.cs @@ -432,7 +432,7 @@ FunctionPointerType functionPointerType Public, false, false, - false, + true, false ); var returnTypeInstance = GetOrCreateStubTypeInstanceFromTypeReference( From 39c4ebff8cfbdd4492e93b6f51727dd25b215a02 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:06:55 +0100 Subject: [PATCH 05/15] refactor: rename TypeFactory to DomainResolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class creates and caches IType, MethodMember, FieldMember, Assembly, and Namespace instances — far more than a type factory. DomainResolver better reflects its primary role: resolving Mono.Cecil references into cached domain objects. This is a pure mechanical rename with no structural or behavioral changes. Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchBuilder.cs | 10 ++--- .../{TypeFactory.cs => DomainResolver.cs} | 4 +- .../AddAttributesAndAttributeDependencies.cs | 10 ++--- .../LoadTasks/AddBaseClassDependency.cs | 8 ++-- .../Loader/LoadTasks/AddClassDependencies.cs | 8 ++-- ArchUnitNET/Loader/LoadTasks/AddMembers.cs | 12 +++--- .../Loader/LoadTasks/AddMethodDependencies.cs | 40 +++++++++---------- .../LoadTasks/CollectAssemblyAttributes.cs | 8 ++-- .../Loader/MonoCecilAttributeExtensions.cs | 16 ++++---- .../Loader/MonoCecilMemberExtensions.cs | 36 ++++++++--------- 10 files changed, 76 insertions(+), 76 deletions(-) rename ArchUnitNET/Loader/{TypeFactory.cs => DomainResolver.cs} (99%) diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index ccdaa2f72..9b05d2a53 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -18,14 +18,14 @@ internal class ArchBuilder private readonly AssemblyRegistry _assemblyRegistry; private readonly LoadTaskRegistry _loadTaskRegistry; private readonly NamespaceRegistry _namespaceRegistry; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public ArchBuilder() { _assemblyRegistry = new AssemblyRegistry(); _namespaceRegistry = new NamespaceRegistry(); _loadTaskRegistry = new LoadTaskRegistry(); - _typeFactory = new TypeFactory( + _domainResolver = new DomainResolver( _loadTaskRegistry, _assemblyRegistry, _namespaceRegistry @@ -54,7 +54,7 @@ public void AddAssembly([NotNull] AssemblyDefinition moduleAssembly, bool isOnly ); _loadTaskRegistry.Add( typeof(CollectAssemblyAttributes), - new CollectAssemblyAttributes(assembly, moduleAssembly, _typeFactory) + new CollectAssemblyAttributes(assembly, moduleAssembly, _domainResolver) ); } } @@ -101,7 +101,7 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) ) .ForEach(typeDefinition => { - var type = _typeFactory.GetOrCreateTypeFromTypeReference(typeDefinition); + var type = _domainResolver.GetOrCreateTypeFromTypeReference(typeDefinition); var assemblyQualifiedName = System.Reflection.Assembly.CreateQualifiedName( module.Assembly.Name.Name, typeDefinition.FullName @@ -150,7 +150,7 @@ public Architecture Build() } UpdateTypeDefinitions(); - var allTypes = _typeFactory.GetAllNonCompilerGeneratedTypes().ToList(); + var allTypes = _domainResolver.GetAllNonCompilerGeneratedTypes().ToList(); var genericParameters = allTypes.OfType().ToList(); var referencedTypes = allTypes.Except(Types).Except(genericParameters); var namespaces = Namespaces.Where(ns => ns.Types.Any()); diff --git a/ArchUnitNET/Loader/TypeFactory.cs b/ArchUnitNET/Loader/DomainResolver.cs similarity index 99% rename from ArchUnitNET/Loader/TypeFactory.cs rename to ArchUnitNET/Loader/DomainResolver.cs index 9aa7d7e82..44513b8d9 100644 --- a/ArchUnitNET/Loader/TypeFactory.cs +++ b/ArchUnitNET/Loader/DomainResolver.cs @@ -13,7 +13,7 @@ namespace ArchUnitNET.Loader { - internal class TypeFactory + internal class DomainResolver { private readonly AssemblyRegistry _assemblyRegistry; private readonly LoadTaskRegistry _loadTaskRegistry; @@ -25,7 +25,7 @@ internal class TypeFactory private readonly Dictionary _allMethods = new Dictionary(); - public TypeFactory( + public DomainResolver( LoadTaskRegistry loadTaskRegistry, AssemblyRegistry assemblyRegistry, NamespaceRegistry namespaceRegistry diff --git a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs index 83b73cf22..2fbbcff6f 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs @@ -18,17 +18,17 @@ internal class AddAttributesAndAttributeDependencies : ILoadTask { private readonly IType _type; private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public AddAttributesAndAttributeDependencies( IType type, TypeDefinition typeDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { _type = type; _typeDefinition = typeDefinition; - _typeFactory = typeFactory; + _domainResolver = domainResolver; } public void Execute() @@ -184,7 +184,7 @@ IEnumerable customAttributes && customAttribute.AttributeType.FullName != "System.Runtime.CompilerServices.NullableContextAttribute" ) - .Select(attr => attr.CreateAttributeFromCustomAttribute(_typeFactory)); + .Select(attr => attr.CreateAttributeFromCustomAttribute(_domainResolver)); } [NotNull] @@ -219,7 +219,7 @@ attributeArgument.Value is TypeReference typeReference ) .ForEach(tuple => { - var argumentType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var argumentType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( tuple.typeReference ); var dependency = new TypeReferenceDependency(_type, argumentType); diff --git a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs b/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs index 9b8a3e299..8ab9a7198 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs @@ -9,19 +9,19 @@ internal class AddBaseClassDependency : ILoadTask private readonly IType _cls; private readonly Type _type; private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public AddBaseClassDependency( IType cls, Type type, TypeDefinition typeDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { _cls = cls; _type = type; _typeDefinition = typeDefinition; - _typeFactory = typeFactory; + _domainResolver = domainResolver; } public void Execute() @@ -33,7 +33,7 @@ public void Execute() return; } - var baseType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var baseType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( typeDefinitionBaseType ); if (!(baseType.Type is Class baseClass)) diff --git a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs index f34eb2112..c4ab6b127 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs @@ -12,18 +12,18 @@ internal class AddClassDependencies : ILoadTask private readonly List _dependencies; private readonly IType _type; private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public AddClassDependencies( IType type, TypeDefinition typeDefinition, - TypeFactory typeFactory, + DomainResolver domainResolver, List dependencies ) { _type = type; _typeDefinition = typeDefinition; - _typeFactory = typeFactory; + _domainResolver = domainResolver; _dependencies = dependencies; } @@ -46,7 +46,7 @@ private void AddInterfaceDependencies() GetInterfacesImplementedByClass(_typeDefinition) .ForEach(target => { - var targetType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var targetType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( target ); _dependencies.Add(new ImplementsInterfaceDependency(_type, targetType)); diff --git a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs index bbb0d86fe..387d8506a 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs @@ -14,18 +14,18 @@ internal class AddMembers : ILoadTask private readonly MemberList _memberList; private readonly ITypeInstance _typeInstance; private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public AddMembers( ITypeInstance typeInstance, TypeDefinition typeDefinition, - TypeFactory typeFactory, + DomainResolver domainResolver, MemberList memberList ) { _typeInstance = typeInstance; _typeDefinition = typeDefinition; - _typeFactory = typeFactory; + _domainResolver = domainResolver; _memberList = memberList; } @@ -46,7 +46,7 @@ private IEnumerable CreateMembers([NotNull] TypeDefinition typeDefiniti .Properties.Select(CreatePropertyMember) .Concat( typeDefinition.Methods.Select(method => - _typeFactory + _domainResolver .GetOrCreateMethodMemberFromMethodReference( _typeInstance, method @@ -62,7 +62,7 @@ private IEnumerable CreateMembers([NotNull] TypeDefinition typeDefiniti private IMember CreateFieldMember([NotNull] FieldDefinition fieldDefinition) { var typeReference = fieldDefinition.FieldType; - var fieldType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var fieldType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( typeReference ); var visibility = GetVisibilityFromFieldDefinition(fieldDefinition); @@ -84,7 +84,7 @@ private IMember CreateFieldMember([NotNull] FieldDefinition fieldDefinition) private IMember CreatePropertyMember(PropertyDefinition propertyDefinition) { var typeReference = propertyDefinition.PropertyType; - var propertyType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var propertyType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( typeReference ); var isCompilerGenerated = propertyDefinition.IsCompilerGenerated(); diff --git a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs index 5878c6d28..7f6946e5c 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs @@ -16,17 +16,17 @@ internal class AddMethodDependencies : ILoadTask { private readonly IType _type; private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public AddMethodDependencies( IType type, TypeDefinition typeDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { _type = type; _typeDefinition = typeDefinition; - _typeFactory = typeFactory; + _domainResolver = domainResolver; } public void Execute() @@ -119,10 +119,10 @@ private IEnumerable CreateMethodSignatureDependencies MethodMember methodMember ) { - var returnType = methodReference.GetReturnType(_typeFactory); + var returnType = methodReference.GetReturnType(_domainResolver); return (returnType != null ? new[] { returnType } : Array.Empty>()) - .Concat(methodReference.GetParameters(_typeFactory)) - .Concat(methodReference.GetGenericParameters(_typeFactory)) + .Concat(methodReference.GetParameters(_domainResolver)) + .Concat(methodReference.GetGenericParameters(_domainResolver)) .Distinct() .Select(signatureType => new MethodSignatureDependency( methodMember, @@ -165,16 +165,16 @@ MethodMember methodMember ); } - bodyTypes.AddRange(methodDefinition.GetBodyTypes(_typeFactory).ToList()); + bodyTypes.AddRange(methodDefinition.GetBodyTypes(_domainResolver).ToList()); - var castTypes = methodDefinition.GetCastTypes(_typeFactory).ToList(); + var castTypes = methodDefinition.GetCastTypes(_domainResolver).ToList(); - var typeCheckTypes = methodDefinition.GetTypeCheckTypes(_typeFactory).ToList(); + var typeCheckTypes = methodDefinition.GetTypeCheckTypes(_domainResolver).ToList(); - var metaDataTypes = methodDefinition.GetMetaDataTypes(_typeFactory).ToList(); + var metaDataTypes = methodDefinition.GetMetaDataTypes(_domainResolver).ToList(); var accessedFieldMembers = methodDefinition - .GetAccessedFieldMembers(_typeFactory) + .GetAccessedFieldMembers(_domainResolver) .ToList(); var calledMethodMembers = CreateMethodBodyDependenciesRecursive( @@ -262,10 +262,10 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc { visitedMethodReferences.Add(calledMethodReference); - var calledType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var calledType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( calledMethodReference.DeclaringType ); - var calledMethodMember = _typeFactory.GetOrCreateMethodMemberFromMethodReference( + var calledMethodMember = _domainResolver.GetOrCreateMethodMemberFromMethodReference( calledType, calledMethodReference ); @@ -302,12 +302,12 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc ); } - bodyTypes.AddRange(calledMethodDefinition.GetBodyTypes(_typeFactory)); - castTypes.AddRange(calledMethodDefinition.GetCastTypes(_typeFactory)); - typeCheckTypes.AddRange(calledMethodDefinition.GetTypeCheckTypes(_typeFactory)); - metaDataTypes.AddRange(calledMethodDefinition.GetMetaDataTypes(_typeFactory)); + bodyTypes.AddRange(calledMethodDefinition.GetBodyTypes(_domainResolver)); + castTypes.AddRange(calledMethodDefinition.GetCastTypes(_domainResolver)); + typeCheckTypes.AddRange(calledMethodDefinition.GetTypeCheckTypes(_domainResolver)); + metaDataTypes.AddRange(calledMethodDefinition.GetMetaDataTypes(_domainResolver)); accessedFieldMembers.AddRange( - calledMethodDefinition.GetAccessedFieldMembers(_typeFactory) + calledMethodDefinition.GetAccessedFieldMembers(_domainResolver) ); foreach ( @@ -360,7 +360,7 @@ ICollection visitedMethodReferences bodyTypes.AddRange( fieldsExceptGeneratorStateInfo.Select(bodyField => - _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) + _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) ) ); } @@ -402,7 +402,7 @@ ICollection visitedMethodReferences bodyTypes.AddRange( fieldsExceptGeneratorStateInfo.Select(bodyField => - _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) + _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) ) ); } diff --git a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs b/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs index c9ffd2bc5..d526ec6e9 100644 --- a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs +++ b/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs @@ -8,24 +8,24 @@ internal class CollectAssemblyAttributes : ILoadTask { private readonly Assembly _assembly; private readonly AssemblyDefinition _assemblyDefinition; - private readonly TypeFactory _typeFactory; + private readonly DomainResolver _domainResolver; public CollectAssemblyAttributes( Assembly assembly, AssemblyDefinition assemblyDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { _assembly = assembly; _assemblyDefinition = assemblyDefinition; - _typeFactory = typeFactory; + _domainResolver = domainResolver; } public void Execute() { var attributeInstances = _assemblyDefinition .CustomAttributes.Select(attr => - attr.CreateAttributeFromCustomAttribute(_typeFactory) + attr.CreateAttributeFromCustomAttribute(_domainResolver) ) .ToList(); _assembly.AttributeInstances.AddRange(attributeInstances); diff --git a/ArchUnitNET/Loader/MonoCecilAttributeExtensions.cs b/ArchUnitNET/Loader/MonoCecilAttributeExtensions.cs index b6ea30095..9b1e41ea7 100644 --- a/ArchUnitNET/Loader/MonoCecilAttributeExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilAttributeExtensions.cs @@ -13,11 +13,11 @@ internal static class MonoCecilAttributeExtensions [NotNull] public static AttributeInstance CreateAttributeFromCustomAttribute( this CustomAttribute customAttribute, - TypeFactory typeFactory + DomainResolver domainResolver ) { var attributeTypeReference = customAttribute.AttributeType; - var attributeType = typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var attributeType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( attributeTypeReference ); var attribute = attributeType.Type as Attribute; @@ -41,7 +41,7 @@ TypeFactory typeFactory { HandleAttributeArgument( constructorArgument, - typeFactory, + domainResolver, out var value, out var type ); @@ -53,7 +53,7 @@ out var type var name = namedArgument.Name; HandleAttributeArgument( namedArgument.Argument, - typeFactory, + domainResolver, out var value, out var type ); @@ -65,7 +65,7 @@ out var type private static void HandleAttributeArgument( CustomAttributeArgument argument, - TypeFactory typeFactory, + DomainResolver domainResolver, out object value, out ITypeInstance type ) @@ -75,21 +75,21 @@ out ITypeInstance type argument = arg; } - type = typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(argument.Type); + type = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(argument.Type); if (argument.Value is IEnumerable attArgEnumerable) { value = ( from attArg in attArgEnumerable select attArg.Value is TypeReference tr - ? typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(tr) + ? domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(tr) : attArg.Value ).ToArray(); } else { value = argument.Value is TypeReference tr - ? typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(tr) + ? domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(tr) : argument.Value; } } diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs index 36027e32c..641aa3892 100644 --- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs @@ -91,36 +91,36 @@ this MethodDefinition methodDefinition internal static ITypeInstance GetReturnType( this MethodReference methodReference, - TypeFactory typeFactory + DomainResolver domainResolver ) => ReturnsVoid(methodReference) ? null - : typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + : domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( methodReference.MethodReturnType.ReturnType ); [NotNull] internal static IEnumerable> GetParameters( this MethodReference method, - TypeFactory typeFactory + DomainResolver domainResolver ) => method.Parameters.Select(parameter => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(parameter.ParameterType) + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(parameter.ParameterType) ); [NotNull] internal static IEnumerable> GetGenericParameters( this MethodReference method, - TypeFactory typeFactory + DomainResolver domainResolver ) => method.GenericParameters.Select( - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference ); [NotNull] internal static IEnumerable> GetBodyTypes( this MethodDefinition methodDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { var instructions = @@ -131,7 +131,7 @@ TypeFactory typeFactory BodyTypeOpCodes.Contains(inst.OpCode) && inst.Operand is TypeReference ) .Select(inst => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( (TypeReference)inst.Operand ) ); @@ -143,7 +143,7 @@ TypeFactory typeFactory methodDefinition.Body?.Variables.Select(variableDefinition => { var variableTypeReference = variableDefinition.VariableType; - return typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + return domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( variableTypeReference ); }) ?? Enumerable.Empty>() @@ -156,7 +156,7 @@ TypeFactory typeFactory [NotNull] internal static IEnumerable> GetCastTypes( this MethodDefinition methodDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { var instructions = @@ -165,7 +165,7 @@ TypeFactory typeFactory return instructions .Where(inst => inst.OpCode == OpCodes.Castclass && inst.Operand is TypeReference) .Select(inst => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( (TypeReference)inst.Operand ) ); @@ -174,7 +174,7 @@ TypeFactory typeFactory [NotNull] internal static IEnumerable> GetMetaDataTypes( this MethodDefinition methodDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { var instructions = @@ -183,7 +183,7 @@ TypeFactory typeFactory return instructions .Where(inst => inst.OpCode == OpCodes.Ldtoken && inst.Operand is TypeReference) .Select(inst => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( (TypeReference)inst.Operand ) ); @@ -192,7 +192,7 @@ TypeFactory typeFactory [NotNull] internal static IEnumerable> GetTypeCheckTypes( this MethodDefinition methodDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { var instructions = @@ -201,7 +201,7 @@ TypeFactory typeFactory return instructions .Where(inst => inst.OpCode == OpCodes.Isinst && inst.Operand is TypeReference) .Select(inst => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( (TypeReference)inst.Operand ) ); @@ -226,7 +226,7 @@ internal static bool IsAsync(this MethodDefinition methodDefinition) [NotNull] internal static IEnumerable GetAccessedFieldMembers( this MethodDefinition methodDefinition, - TypeFactory typeFactory + DomainResolver domainResolver ) { var accessedFieldMembers = new List(); @@ -239,7 +239,7 @@ TypeFactory typeFactory foreach (var fieldReference in accessedFieldReferences) { - var declaringType = typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( + var declaringType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( fieldReference.DeclaringType ); var matchingFieldMembers = declaringType @@ -250,7 +250,7 @@ TypeFactory typeFactory switch (matchingFieldMembers.Count) { case 0: - var stubFieldMember = typeFactory.CreateStubFieldMemberFromFieldReference( + var stubFieldMember = domainResolver.CreateStubFieldMemberFromFieldReference( declaringType.Type, fieldReference ); From 7fea6aabc3c3553ccfeca4b0614d3a57e3d3edbe Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:12:47 +0100 Subject: [PATCH 06/15] refactor: inline AssemblyRegistry, NamespaceRegistry, and RegistryUtils into DomainResolver Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchBuilder.cs | 16 ++--- ArchUnitNET/Loader/AssemblyRegistry.cs | 37 ----------- ArchUnitNET/Loader/DomainResolver.cs | 85 +++++++++++++++++++------ ArchUnitNET/Loader/NamespaceRegistry.cs | 22 ------- ArchUnitNET/Loader/RegistryUtils.cs | 25 -------- 5 files changed, 71 insertions(+), 114 deletions(-) delete mode 100644 ArchUnitNET/Loader/AssemblyRegistry.cs delete mode 100644 ArchUnitNET/Loader/NamespaceRegistry.cs delete mode 100644 ArchUnitNET/Loader/RegistryUtils.cs diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index 9b05d2a53..4e462a17b 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -15,28 +15,22 @@ internal class ArchBuilder private readonly ArchitectureCacheKey _architectureCacheKey; private readonly IDictionary _architectureTypes = new Dictionary(); - private readonly AssemblyRegistry _assemblyRegistry; private readonly LoadTaskRegistry _loadTaskRegistry; - private readonly NamespaceRegistry _namespaceRegistry; private readonly DomainResolver _domainResolver; public ArchBuilder() { - _assemblyRegistry = new AssemblyRegistry(); - _namespaceRegistry = new NamespaceRegistry(); _loadTaskRegistry = new LoadTaskRegistry(); _domainResolver = new DomainResolver( - _loadTaskRegistry, - _assemblyRegistry, - _namespaceRegistry + _loadTaskRegistry ); _architectureCacheKey = new ArchitectureCacheKey(); _architectureCache = ArchitectureCache.Instance; } public IEnumerable Types => _architectureTypes.Values; - public IEnumerable Assemblies => _assemblyRegistry.Assemblies; - public IEnumerable Namespaces => _namespaceRegistry.Namespaces; + public IEnumerable Assemblies => _domainResolver.Assemblies; + public IEnumerable Namespaces => _domainResolver.Namespaces; public void AddAssembly([NotNull] AssemblyDefinition moduleAssembly, bool isOnlyReferenced) { @@ -44,9 +38,9 @@ public void AddAssembly([NotNull] AssemblyDefinition moduleAssembly, bool isOnly .MainModule.AssemblyReferences.Select(reference => reference.Name) .ToList(); - if (!_assemblyRegistry.ContainsAssembly(moduleAssembly.FullName)) + if (!_domainResolver.ContainsAssembly(moduleAssembly.FullName)) { - var assembly = _assemblyRegistry.GetOrCreateAssembly( + var assembly = _domainResolver.GetOrCreateAssembly( moduleAssembly.Name.Name, moduleAssembly.FullName, isOnlyReferenced, diff --git a/ArchUnitNET/Loader/AssemblyRegistry.cs b/ArchUnitNET/Loader/AssemblyRegistry.cs deleted file mode 100644 index 4b7469301..000000000 --- a/ArchUnitNET/Loader/AssemblyRegistry.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using ArchUnitNET.Domain; - -namespace ArchUnitNET.Loader -{ - internal class AssemblyRegistry - { - private readonly Dictionary _assemblies = - new Dictionary(); - - public IEnumerable Assemblies => _assemblies.Values; - - public Assembly GetOrCreateAssembly( - string assemblyName, - string assemblyFullName, - bool isOnlyReferenced, - List assemblyReferences - ) - { - return RegistryUtils.GetFromDictOrCreateAndAdd( - assemblyFullName, - _assemblies, - s => new Assembly( - assemblyName, - assemblyFullName, - isOnlyReferenced, - assemblyReferences - ) - ); - } - - public bool ContainsAssembly(string assemblyName) - { - return _assemblies.ContainsKey(assemblyName); - } - } -} diff --git a/ArchUnitNET/Loader/DomainResolver.cs b/ArchUnitNET/Loader/DomainResolver.cs index 44513b8d9..833b4f8c4 100644 --- a/ArchUnitNET/Loader/DomainResolver.cs +++ b/ArchUnitNET/Loader/DomainResolver.cs @@ -15,9 +15,13 @@ namespace ArchUnitNET.Loader { internal class DomainResolver { - private readonly AssemblyRegistry _assemblyRegistry; private readonly LoadTaskRegistry _loadTaskRegistry; - private readonly NamespaceRegistry _namespaceRegistry; + + private readonly Dictionary _assemblies = + new Dictionary(); + + private readonly Dictionary _namespaces = + new Dictionary(); private readonly Dictionary> _allTypes = new Dictionary>(); @@ -26,14 +30,55 @@ internal class DomainResolver new Dictionary(); public DomainResolver( - LoadTaskRegistry loadTaskRegistry, - AssemblyRegistry assemblyRegistry, - NamespaceRegistry namespaceRegistry + LoadTaskRegistry loadTaskRegistry ) { _loadTaskRegistry = loadTaskRegistry; - _assemblyRegistry = assemblyRegistry; - _namespaceRegistry = namespaceRegistry; + } + + public IEnumerable Assemblies => _assemblies.Values; + + public IEnumerable Namespaces => _namespaces.Values; + + public IEnumerable> Types => _allTypes.Values; + + internal Assembly GetOrCreateAssembly( + string assemblyName, + string assemblyFullName, + bool isOnlyReferenced, + List assemblyReferences + ) + { + if (_assemblies.TryGetValue(assemblyFullName, out var existing)) + { + return existing; + } + + var assembly = new Assembly( + assemblyName, + assemblyFullName, + isOnlyReferenced, + assemblyReferences + ); + _assemblies.Add(assemblyFullName, assembly); + return assembly; + } + + internal bool ContainsAssembly(string assemblyFullName) + { + return _assemblies.ContainsKey(assemblyFullName); + } + + internal Namespace GetOrCreateNamespace(string namespaceName) + { + if (_namespaces.TryGetValue(namespaceName, out var existing)) + { + return existing; + } + + var ns = new Namespace(namespaceName, new List()); + _namespaces.Add(namespaceName, ns); + return ns; } public IEnumerable GetAllNonCompilerGeneratedTypes() @@ -263,10 +308,10 @@ bool isStub { declaringTypeReference = declaringTypeReference.DeclaringType; } - var currentNamespace = _namespaceRegistry.GetOrCreateNamespace( + var currentNamespace = GetOrCreateNamespace( declaringTypeReference.Namespace ); - var currentAssembly = _assemblyRegistry.GetOrCreateAssembly( + var currentAssembly = GetOrCreateAssembly( assemblyFullName, assemblyFullName, true, @@ -360,13 +405,13 @@ TypeReference typeReference new Type( typeReference.BuildFullName(), typeReference.Name, - _assemblyRegistry.GetOrCreateAssembly( + GetOrCreateAssembly( typeReference.Scope.Name, typeReference.Scope.ToString(), true, null ), - _namespaceRegistry.GetOrCreateNamespace(typeReference.Namespace), + GetOrCreateNamespace(typeReference.Namespace), NotAccessible, typeReference.IsNested, typeReference.HasGenericParameters, @@ -459,15 +504,17 @@ [NotNull] MethodReference methodReference var methodReferenceFullName = methodReference.BuildFullName(); if (methodReference.IsGenericInstance) { - return RegistryUtils.GetFromDictOrCreateAndAdd( - methodReferenceFullName, - _allMethods, - _ => - CreateGenericInstanceMethodMemberFromMethodReference( - typeInstance, - methodReference - ) + if (_allMethods.TryGetValue(methodReferenceFullName, out var existingGenericInstance)) + { + return existingGenericInstance; + } + + var genericInstance = CreateGenericInstanceMethodMemberFromMethodReference( + typeInstance, + methodReference ); + _allMethods.Add(methodReferenceFullName, genericInstance); + return genericInstance; } if (_allMethods.TryGetValue(methodReferenceFullName, out var existingMethodInstance)) diff --git a/ArchUnitNET/Loader/NamespaceRegistry.cs b/ArchUnitNET/Loader/NamespaceRegistry.cs deleted file mode 100644 index c2a39bb5b..000000000 --- a/ArchUnitNET/Loader/NamespaceRegistry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using ArchUnitNET.Domain; - -namespace ArchUnitNET.Loader -{ - internal class NamespaceRegistry - { - private readonly Dictionary _namespaces = - new Dictionary(); - - public IEnumerable Namespaces => _namespaces.Values; - - public Namespace GetOrCreateNamespace(string typeNamespaceName) - { - return RegistryUtils.GetFromDictOrCreateAndAdd( - typeNamespaceName, - _namespaces, - s => new Namespace(typeNamespaceName, new List()) - ); - } - } -} diff --git a/ArchUnitNET/Loader/RegistryUtils.cs b/ArchUnitNET/Loader/RegistryUtils.cs deleted file mode 100644 index f31afa94c..000000000 --- a/ArchUnitNET/Loader/RegistryUtils.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace ArchUnitNET.Loader -{ - internal static class RegistryUtils - { - public static T GetFromDictOrCreateAndAdd( - TK key, - Dictionary dict, - Func createFunc - ) - { - if (dict.TryGetValue(key, out var value)) - { - return value; - } - - value = createFunc(key); - dict.Add(key, value); - - return value; - } - } -} From 277dd42062545bf04cfb0d392a203b48775c97bf Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:15:25 +0100 Subject: [PATCH 07/15] refactor: add field member caching to DomainResolver Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/DomainResolver.cs | 75 +++++++++++++++++-- ArchUnitNET/Loader/LoadTasks/AddMembers.cs | 71 +----------------- .../Loader/MonoCecilMemberExtensions.cs | 2 +- 3 files changed, 71 insertions(+), 77 deletions(-) diff --git a/ArchUnitNET/Loader/DomainResolver.cs b/ArchUnitNET/Loader/DomainResolver.cs index 833b4f8c4..838427edf 100644 --- a/ArchUnitNET/Loader/DomainResolver.cs +++ b/ArchUnitNET/Loader/DomainResolver.cs @@ -29,6 +29,9 @@ internal class DomainResolver private readonly Dictionary _allMethods = new Dictionary(); + private readonly Dictionary _allFields = + new Dictionary(); + public DomainResolver( LoadTaskRegistry loadTaskRegistry ) @@ -620,33 +623,89 @@ MethodReference methodReference } [NotNull] - internal FieldMember CreateStubFieldMemberFromFieldReference( + internal FieldMember GetOrCreateFieldMember( [NotNull] IType type, [NotNull] FieldReference fieldReference ) { + var fullName = fieldReference.FullName; + if (_allFields.TryGetValue(fullName, out var existing)) + { + return existing; + } + var typeReference = fieldReference.FieldType; var fieldType = GetOrCreateStubTypeInstanceFromTypeReference(typeReference); var isCompilerGenerated = fieldReference.IsCompilerGenerated(); - bool? isStatic = null; - var isReadOnly = false; + Visibility visibility; + bool? isStatic; + Writability writeAccessor; if (fieldReference is FieldDefinition fieldDefinition) { + visibility = GetVisibilityFromFieldDefinition(fieldDefinition); isStatic = fieldDefinition.IsStatic; - isReadOnly = fieldDefinition.IsInitOnly; + writeAccessor = fieldDefinition.IsInitOnly + ? Writability.ReadOnly + : Writability.Writable; + } + else + { + visibility = Public; + isStatic = null; + writeAccessor = Writability.Writable; } - return new FieldMember( + var fieldMember = new FieldMember( type, fieldReference.Name, - fieldReference.FullName, - Public, + fullName, + visibility, fieldType, isCompilerGenerated, isStatic, - isReadOnly ? Writability.ReadOnly : Writability.Writable + writeAccessor ); + + _allFields.Add(fullName, fieldMember); + return fieldMember; + } + + private static Visibility GetVisibilityFromFieldDefinition( + [NotNull] FieldDefinition fieldDefinition + ) + { + if (fieldDefinition.IsPublic) + { + return Public; + } + + if (fieldDefinition.IsPrivate) + { + return Private; + } + + if (fieldDefinition.IsFamily) + { + return Protected; + } + + if (fieldDefinition.IsAssembly) + { + return Internal; + } + + if (fieldDefinition.IsFamilyOrAssembly) + { + return ProtectedInternal; + } + + if (fieldDefinition.IsFamilyAndAssembly) + { + return PrivateProtected; + } + + throw new ArgumentException("The field definition seems to have no visibility."); } public IEnumerable GetGenericParameters( diff --git a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs index 387d8506a..c9b887f92 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs @@ -1,11 +1,8 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using ArchUnitNET.Domain; using JetBrains.Annotations; using Mono.Cecil; -using static ArchUnitNET.Domain.Visibility; namespace ArchUnitNET.Loader.LoadTasks { @@ -40,7 +37,9 @@ private IEnumerable CreateMembers([NotNull] TypeDefinition typeDefiniti { return typeDefinition .Fields.Where(fieldDefinition => !fieldDefinition.IsBackingField()) - .Select(CreateFieldMember) + .Select(fieldDef => + (IMember)_domainResolver.GetOrCreateFieldMember(_typeInstance.Type, fieldDef) + ) .Concat( typeDefinition .Properties.Select(CreatePropertyMember) @@ -58,28 +57,6 @@ private IEnumerable CreateMembers([NotNull] TypeDefinition typeDefiniti .Where(member => !member.IsCompilerGenerated); } - [NotNull] - private IMember CreateFieldMember([NotNull] FieldDefinition fieldDefinition) - { - var typeReference = fieldDefinition.FieldType; - var fieldType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - typeReference - ); - var visibility = GetVisibilityFromFieldDefinition(fieldDefinition); - var isCompilerGenerated = fieldDefinition.IsCompilerGenerated(); - var writeAccessor = GetWriteAccessor(fieldDefinition); - return new FieldMember( - _typeInstance.Type, - fieldDefinition.Name, - fieldDefinition.FullName, - visibility, - fieldType, - isCompilerGenerated, - fieldDefinition.IsStatic, - writeAccessor - ); - } - [NotNull] private IMember CreatePropertyMember(PropertyDefinition propertyDefinition) { @@ -103,48 +80,6 @@ private IMember CreatePropertyMember(PropertyDefinition propertyDefinition) ); } - private static Visibility GetVisibilityFromFieldDefinition( - [NotNull] FieldDefinition fieldDefinition - ) - { - if (fieldDefinition.IsPublic) - { - return Public; - } - - if (fieldDefinition.IsPrivate) - { - return Private; - } - - if (fieldDefinition.IsFamily) - { - return Protected; - } - - if (fieldDefinition.IsAssembly) - { - return Internal; - } - - if (fieldDefinition.IsFamilyOrAssembly) - { - return ProtectedInternal; - } - - if (fieldDefinition.IsFamilyAndAssembly) - { - return PrivateProtected; - } - - throw new ArgumentException("The field definition seems to have no visibility."); - } - - private static Writability GetWriteAccessor([NotNull] FieldDefinition fieldDefinition) - { - return fieldDefinition.IsInitOnly ? Writability.ReadOnly : Writability.Writable; - } - private static Writability GetWriteAccessor([NotNull] PropertyDefinition propertyDefinition) { bool isReadOnly = propertyDefinition.SetMethod == null; diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs index 641aa3892..aa413e37d 100644 --- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs @@ -250,7 +250,7 @@ DomainResolver domainResolver switch (matchingFieldMembers.Count) { case 0: - var stubFieldMember = domainResolver.CreateStubFieldMemberFromFieldReference( + var stubFieldMember = domainResolver.GetOrCreateFieldMember( declaringType.Type, fieldReference ); From 6bf33329ccf9f04b594e1a20d1d93d3756a9a618 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:19:36 +0100 Subject: [PATCH 08/15] refactor: replace separate method body scanning methods with single-pass ScanMethodBody Signed-off-by: Alexander Linne --- .../Loader/LoadTasks/AddMethodDependencies.cs | 26 +- .../Loader/MonoCecilMemberExtensions.cs | 258 ++++++++++-------- 2 files changed, 151 insertions(+), 133 deletions(-) diff --git a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs index 7f6946e5c..03d1948a5 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs @@ -165,17 +165,16 @@ MethodMember methodMember ); } - bodyTypes.AddRange(methodDefinition.GetBodyTypes(_domainResolver).ToList()); + var scan = methodDefinition.ScanMethodBody(_domainResolver); + bodyTypes.AddRange(scan.BodyTypes); - var castTypes = methodDefinition.GetCastTypes(_domainResolver).ToList(); + var castTypes = scan.CastTypes; - var typeCheckTypes = methodDefinition.GetTypeCheckTypes(_domainResolver).ToList(); + var typeCheckTypes = scan.TypeCheckTypes; - var metaDataTypes = methodDefinition.GetMetaDataTypes(_domainResolver).ToList(); + var metaDataTypes = scan.MetaDataTypes; - var accessedFieldMembers = methodDefinition - .GetAccessedFieldMembers(_domainResolver) - .ToList(); + var accessedFieldMembers = scan.AccessedFieldMembers; var calledMethodMembers = CreateMethodBodyDependenciesRecursive( methodBody, @@ -302,13 +301,12 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc ); } - bodyTypes.AddRange(calledMethodDefinition.GetBodyTypes(_domainResolver)); - castTypes.AddRange(calledMethodDefinition.GetCastTypes(_domainResolver)); - typeCheckTypes.AddRange(calledMethodDefinition.GetTypeCheckTypes(_domainResolver)); - metaDataTypes.AddRange(calledMethodDefinition.GetMetaDataTypes(_domainResolver)); - accessedFieldMembers.AddRange( - calledMethodDefinition.GetAccessedFieldMembers(_domainResolver) - ); + var calledScan = calledMethodDefinition.ScanMethodBody(_domainResolver); + bodyTypes.AddRange(calledScan.BodyTypes); + castTypes.AddRange(calledScan.CastTypes); + typeCheckTypes.AddRange(calledScan.TypeCheckTypes); + metaDataTypes.AddRange(calledScan.MetaDataTypes); + accessedFieldMembers.AddRange(calledScan.AccessedFieldMembers); foreach ( var dep in CreateMethodBodyDependenciesRecursive( diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs index aa413e37d..061dc5a28 100644 --- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs +++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Exceptions; using ArchUnitNET.Domain.Extensions; using JetBrains.Annotations; using Mono.Cecil; @@ -117,94 +116,161 @@ DomainResolver domainResolver domainResolver.GetOrCreateStubTypeInstanceFromTypeReference ); + /// + /// Result of a single-pass scan of a method body. + /// + internal struct MethodBodyScanResult + { + internal readonly List> BodyTypes; + internal readonly List> CastTypes; + internal readonly List> MetaDataTypes; + internal readonly List> TypeCheckTypes; + internal readonly List AccessedFieldMembers; + + internal MethodBodyScanResult( + List> bodyTypes, + List> castTypes, + List> metaDataTypes, + List> typeCheckTypes, + List accessedFieldMembers + ) + { + BodyTypes = bodyTypes; + CastTypes = castTypes; + MetaDataTypes = metaDataTypes; + TypeCheckTypes = typeCheckTypes; + AccessedFieldMembers = accessedFieldMembers; + } + } + + /// + /// Scans the method body in a single pass, collecting body types, cast types, + /// metadata types, type-check types, accessed field members, and local variable types. + /// [NotNull] - internal static IEnumerable> GetBodyTypes( + internal static MethodBodyScanResult ScanMethodBody( this MethodDefinition methodDefinition, DomainResolver domainResolver ) { - var instructions = - methodDefinition.Body?.Instructions ?? Enumerable.Empty(); + var bodyTypes = new List>(); + var castTypes = new List>(); + var metaDataTypes = new List>(); + var typeCheckTypes = new List>(); + var accessedFieldMembers = new List(); - var bodyTypes = instructions - .Where(inst => - BodyTypeOpCodes.Contains(inst.OpCode) && inst.Operand is TypeReference - ) - .Select(inst => - domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - (TypeReference)inst.Operand - ) + var body = methodDefinition.Body; + if (body != null) + { + // Collect local variable types + var seenBodyTypes = new HashSet>(); + bodyTypes.AddRange( + body.Variables.Select(variableDefinition => + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + variableDefinition.VariableType + ) + ) + .Where(typeInstance => seenBodyTypes.Add(typeInstance)) ); - //OpCodes.Ldstr should create a dependency to string, but it does not have a TypeReference as Operand so no Type can be created + // Single pass over instructions + var seenFieldRefs = new HashSet( + FieldReferenceNameComparer.Instance + ); + foreach (var instruction in body.Instructions) + { + var opCode = instruction.OpCode; + var operand = instruction.Operand; - bodyTypes = bodyTypes - .Union( - methodDefinition.Body?.Variables.Select(variableDefinition => + switch (operand) { - var variableTypeReference = variableDefinition.VariableType; - return domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - variableTypeReference - ); - }) ?? Enumerable.Empty>() - ) - .Distinct(); + case TypeReference typeReference when opCode == OpCodes.Castclass: + castTypes.Add( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + typeReference + ) + ); + break; + case TypeReference typeReference when opCode == OpCodes.Ldtoken: + metaDataTypes.Add( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + typeReference + ) + ); + break; + case TypeReference typeReference when opCode == OpCodes.Isinst: + typeCheckTypes.Add( + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + typeReference + ) + ); + break; + case TypeReference typeReference: + { + if (BodyTypeOpCodes.Contains(opCode)) + { + var bodyTypeInstance = + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + typeReference + ); + if (seenBodyTypes.Add(bodyTypeInstance)) + { + bodyTypes.Add(bodyTypeInstance); + } + } + + break; + } + case FieldReference fieldReference when !seenFieldRefs.Add(fieldReference): + continue; + case FieldReference fieldReference: + { + var declaringType = + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + fieldReference.DeclaringType + ); + accessedFieldMembers.Add( + domainResolver.GetOrCreateFieldMember( + declaringType.Type, + fieldReference + ) + ); + break; + } + } + } + } - return bodyTypes; + return new MethodBodyScanResult( + bodyTypes, + castTypes, + metaDataTypes, + typeCheckTypes, + accessedFieldMembers + ); } - [NotNull] - internal static IEnumerable> GetCastTypes( - this MethodDefinition methodDefinition, - DomainResolver domainResolver - ) + /// + /// Comparer that deduplicates FieldReference by full name, avoiding repeated field lookups. + /// + private sealed class FieldReferenceNameComparer : IEqualityComparer { - var instructions = - methodDefinition.Body?.Instructions ?? Enumerable.Empty(); - - return instructions - .Where(inst => inst.OpCode == OpCodes.Castclass && inst.Operand is TypeReference) - .Select(inst => - domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - (TypeReference)inst.Operand - ) - ); - } + internal static readonly FieldReferenceNameComparer Instance = + new FieldReferenceNameComparer(); - [NotNull] - internal static IEnumerable> GetMetaDataTypes( - this MethodDefinition methodDefinition, - DomainResolver domainResolver - ) - { - var instructions = - methodDefinition.Body?.Instructions ?? Enumerable.Empty(); - - return instructions - .Where(inst => inst.OpCode == OpCodes.Ldtoken && inst.Operand is TypeReference) - .Select(inst => - domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - (TypeReference)inst.Operand - ) - ); - } + public bool Equals(FieldReference x, FieldReference y) + { + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; + return x.FullName == y.FullName; + } - [NotNull] - internal static IEnumerable> GetTypeCheckTypes( - this MethodDefinition methodDefinition, - DomainResolver domainResolver - ) - { - var instructions = - methodDefinition.Body?.Instructions ?? Enumerable.Empty(); - - return instructions - .Where(inst => inst.OpCode == OpCodes.Isinst && inst.Operand is TypeReference) - .Select(inst => - domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - (TypeReference)inst.Operand - ) - ); + public int GetHashCode(FieldReference obj) + { + return obj?.FullName?.GetHashCode() ?? 0; + } } internal static bool IsIterator(this MethodDefinition methodDefinition) @@ -223,52 +289,6 @@ internal static bool IsAsync(this MethodDefinition methodDefinition) ); } - [NotNull] - internal static IEnumerable GetAccessedFieldMembers( - this MethodDefinition methodDefinition, - DomainResolver domainResolver - ) - { - var accessedFieldMembers = new List(); - var instructions = - methodDefinition.Body?.Instructions.ToList() ?? new List(); - var accessedFieldReferences = instructions - .Select(inst => inst.Operand) - .OfType() - .Distinct(); - - foreach (var fieldReference in accessedFieldReferences) - { - var declaringType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - fieldReference.DeclaringType - ); - var matchingFieldMembers = declaringType - .Type.GetFieldMembers() - .Where(member => member.Name == fieldReference.Name) - .ToList(); - - switch (matchingFieldMembers.Count) - { - case 0: - var stubFieldMember = domainResolver.GetOrCreateFieldMember( - declaringType.Type, - fieldReference - ); - accessedFieldMembers.Add(stubFieldMember); - break; - case 1: - accessedFieldMembers.Add(matchingFieldMembers.First()); - break; - default: - throw new MultipleOccurrencesInSequenceException( - $"Multiple Fields matching {fieldReference.FullName} found in provided type." - ); - } - } - - return accessedFieldMembers.Distinct(); - } - internal static bool IsCompilerGenerated(this MemberReference memberReference) { if (memberReference.Name.HasCompilerGeneratedName()) From 9484dd41670600e71c38efb39a8f6630c7abfb6f Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:37:42 +0100 Subject: [PATCH 09/15] refactor: convert load tasks to static classes and remove LoadTaskRegistry Replace ILoadTask interface and LoadTaskRegistry with static Execute methods on each load task class. Move type processing orchestration from DomainResolver into ArchBuilder.ProcessTypes() with explicit phased execution. Remove RegexUtils (property matching now uses dictionary lookup in AddMethodDependencies). Remove LoadBaseTask/LoadNonBaseTasks from DomainResolver since type processing is now driven externally by ArchBuilder. Remove RegexUtils tests (only BackingFieldExamples test fixture class retained). Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchBuilder.cs | 255 ++++++++++++++---- ArchUnitNET/Loader/DomainResolver.cs | 206 ++++++-------- ArchUnitNET/Loader/LoadTaskRegistry.cs | 87 ------ .../AddAttributesAndAttributeDependencies.cs | 227 +++++++++------- .../LoadTasks/AddBackwardsDependencies.cs | 20 +- .../LoadTasks/AddBaseClassDependency.cs | 38 +-- .../Loader/LoadTasks/AddClassDependencies.cs | 52 ++-- .../AddFieldAndPropertyDependencies.cs | 39 ++- .../AddGenericArgumentDependencies.cs | 72 +++-- .../AddGenericParameterDependencies.cs | 36 +-- ArchUnitNET/Loader/LoadTasks/AddMembers.cs | 150 +++++++---- .../Loader/LoadTasks/AddMethodDependencies.cs | 210 +++++---------- .../Loader/LoadTasks/AddTypesToNamespaces.cs | 18 +- .../LoadTasks/CollectAssemblyAttributes.cs | 28 +- ArchUnitNET/Loader/LoadTasks/ILoadTask.cs | 7 - ArchUnitNET/Loader/RegexUtils.cs | 39 --- .../Domain/Extensions/TypeExtensionTests.cs | 2 +- ArchUnitNETTests/Loader/RegexUtilsTests.cs | 144 ---------- 18 files changed, 703 insertions(+), 927 deletions(-) delete mode 100644 ArchUnitNET/Loader/LoadTaskRegistry.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/ILoadTask.cs delete mode 100644 ArchUnitNET/Loader/RegexUtils.cs diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index 4e462a17b..793336837 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -9,50 +9,82 @@ namespace ArchUnitNET.Loader { + /// + /// Internal builder that constructs an from loaded modules. + /// Coordinates type discovery (via ), type processing + /// (via the LoadTasks static classes), and architecture assembly. Manages the + /// lookup. + /// internal class ArchBuilder { private readonly ArchitectureCache _architectureCache; private readonly ArchitectureCacheKey _architectureCacheKey; - private readonly IDictionary _architectureTypes = - new Dictionary(); - private readonly LoadTaskRegistry _loadTaskRegistry; private readonly DomainResolver _domainResolver; + /// + /// Non-compiler-generated types paired with their Cecil s, + /// collected during and consumed by + /// to populate members, dependencies, and attributes. + /// Keyed by assembly-qualified name for deduplication. + /// + private readonly Dictionary< + string, + (ITypeInstance TypeInstance, TypeDefinition Definition) + > _typesToProcess = + new Dictionary< + string, + (ITypeInstance TypeInstance, TypeDefinition Definition) + >(); + + /// + /// Assemblies paired with their Cecil s, + /// registered via . Keyed by assembly full name + /// for deduplication. Consumed by Phase 5 (assembly attribute collection). + /// + private readonly Dictionary< + string, + (Assembly Assembly, AssemblyDefinition Definition) + > _assemblyData = + new Dictionary(); + public ArchBuilder() { - _loadTaskRegistry = new LoadTaskRegistry(); - _domainResolver = new DomainResolver( - _loadTaskRegistry - ); + _domainResolver = new DomainResolver(); _architectureCacheKey = new ArchitectureCacheKey(); _architectureCache = ArchitectureCache.Instance; } - public IEnumerable Types => _architectureTypes.Values; - public IEnumerable Assemblies => _domainResolver.Assemblies; - public IEnumerable Namespaces => _domainResolver.Namespaces; - + /// + /// Registers an assembly for attribute collection during . + /// Skips assemblies that have already been registered. + /// public void AddAssembly([NotNull] AssemblyDefinition moduleAssembly, bool isOnlyReferenced) { + if (_assemblyData.ContainsKey(moduleAssembly.FullName)) + { + return; + } + var references = moduleAssembly .MainModule.AssemblyReferences.Select(reference => reference.Name) .ToList(); - if (!_domainResolver.ContainsAssembly(moduleAssembly.FullName)) - { - var assembly = _domainResolver.GetOrCreateAssembly( - moduleAssembly.Name.Name, - moduleAssembly.FullName, - isOnlyReferenced, - references - ); - _loadTaskRegistry.Add( - typeof(CollectAssemblyAttributes), - new CollectAssemblyAttributes(assembly, moduleAssembly, _domainResolver) - ); - } + var assembly = _domainResolver.GetOrCreateAssembly( + moduleAssembly.Name.Name, + moduleAssembly.FullName, + isOnlyReferenced, + references + ); + _assemblyData.Add(moduleAssembly.FullName, (assembly, moduleAssembly)); } + /// + /// Discovers types in the given , creates domain type instances + /// via , and records them for later processing. + /// Filters out compiler-generated types, code-coverage instrumentation types, + /// nullable context attributes, and types outside the optional + /// . + /// public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) { _architectureCacheKey.Add(module.Name, namespaceFilter); @@ -81,7 +113,6 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) types.AddRange(nestedTypes); } - var currentTypes = new List(types.Count); types .Where(typeDefinition => ( @@ -95,46 +126,28 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) ) .ForEach(typeDefinition => { - var type = _domainResolver.GetOrCreateTypeFromTypeReference(typeDefinition); + var typeInstance = _domainResolver.GetOrCreateTypeInstanceFromTypeReference( + typeDefinition + ); + var type = typeInstance.Type; var assemblyQualifiedName = System.Reflection.Assembly.CreateQualifiedName( module.Assembly.Name.Name, typeDefinition.FullName ); if ( - !_architectureTypes.ContainsKey(assemblyQualifiedName) + !_typesToProcess.ContainsKey(assemblyQualifiedName) && !type.IsCompilerGenerated ) { - currentTypes.Add(type); - _architectureTypes.Add(assemblyQualifiedName, type); + _typesToProcess.Add(assemblyQualifiedName, (typeInstance, typeDefinition)); } }); - - _loadTaskRegistry.Add( - typeof(AddTypesToNamespaces), - new AddTypesToNamespaces(currentTypes) - ); - } - - private void UpdateTypeDefinitions() - { - _loadTaskRegistry.ExecuteTasks( - new List - { - typeof(AddMembers), - typeof(AddGenericParameterDependencies), - typeof(AddAttributesAndAttributeDependencies), - typeof(CollectAssemblyAttributes), - typeof(AddFieldAndPropertyDependencies), - typeof(AddMethodDependencies), - typeof(AddGenericArgumentDependencies), - typeof(AddClassDependencies), - typeof(AddBackwardsDependencies), - typeof(AddTypesToNamespaces), - } - ); } + /// + /// Builds the from all loaded modules. Returns a cached + /// instance when available. + /// public Architecture Build() { var architecture = _architectureCache.TryGetArchitecture(_architectureCacheKey); @@ -143,20 +156,144 @@ public Architecture Build() return architecture; } - UpdateTypeDefinitions(); - var allTypes = _domainResolver.GetAllNonCompilerGeneratedTypes().ToList(); + ProcessTypes(); + + var allTypes = _domainResolver + .Types.Select(instance => instance.Type) + .Where(type => !type.IsCompilerGenerated) + .Distinct() + .ToList(); + var types = allTypes + .Where(type => !type.IsStub && !(type is GenericParameter)) + .ToList(); var genericParameters = allTypes.OfType().ToList(); - var referencedTypes = allTypes.Except(Types).Except(genericParameters); - var namespaces = Namespaces.Where(ns => ns.Types.Any()); + var referencedTypes = allTypes + .Where(type => type.IsStub && !(type is GenericParameter)) + .ToList(); + var namespaces = _domainResolver.Namespaces.Where(ns => ns.Types.Any()); var newArchitecture = new Architecture( - Assemblies, + _domainResolver.Assemblies, namespaces, - Types, + types, genericParameters, referencedTypes ); + _architectureCache.Add(_architectureCacheKey, newArchitecture); + return newArchitecture; } + + /// + /// Runs all type-processing phases in the required order across every discovered type. + /// Each phase must complete for all types before the next phase begins, because later + /// phases depend on data populated by earlier ones (e.g. members must exist before + /// method dependencies can be resolved). + /// + private void ProcessTypes() + { + var typesToProcess = _typesToProcess.Values; + + // Phase 1: Base class dependency (non-interface types only) + foreach (var entry in typesToProcess.Where(entry => !entry.Definition.IsInterface)) + { + AddBaseClassDependency.Execute( + entry.TypeInstance.Type, + entry.Definition, + _domainResolver + ); + } + + // Phase 2: Members (fields, properties, methods) + // Collect (TypeInstance, Definition, MemberData) for use in Phases 4 and 7. + var typesWithMemberData = new List<( + ITypeInstance TypeInstance, + TypeDefinition TypeDef, + MemberData MemberData + )>(_typesToProcess.Count); + typesWithMemberData.AddRange( + from entry in typesToProcess + let memberData = AddMembers.Execute( + entry.TypeInstance, + entry.Definition, + _domainResolver + ) + select (entry.TypeInstance, entry.Definition, memberData) + ); + + // Phase 3: Generic parameter dependencies + foreach (var entry in typesToProcess) + { + AddGenericParameterDependencies.Execute(entry.TypeInstance.Type); + } + + // Phase 4: Attributes and attribute dependencies + foreach (var entry in typesWithMemberData) + { + AddAttributesAndAttributeDependencies.Execute( + entry.TypeInstance.Type, + entry.TypeDef, + _domainResolver, + entry.MemberData.MethodPairs + ); + } + + // Phase 5: Assembly-level attributes + // Materialized to a list because CollectAssemblyAttributes can trigger + // GetOrCreateAssembly in DomainResolver, which would invalidate a lazy query. + var assemblyData = _assemblyData.Values.ToList(); + foreach (var entry in assemblyData) + { + CollectAssemblyAttributes.Execute( + entry.Assembly, + entry.Definition, + _domainResolver + ); + } + + // Phase 6: Field and property type dependencies + foreach (var entry in typesToProcess) + { + AddFieldAndPropertyDependencies.Execute(entry.TypeInstance.Type); + } + + // Phase 7: Method signature and body dependencies + foreach (var entry in typesWithMemberData) + { + AddMethodDependencies.Execute( + entry.TypeInstance.Type, + _domainResolver, + entry.MemberData.MethodPairs, + entry.MemberData.PropertyByAccessor + ); + } + + // Phase 8: Generic argument dependencies + foreach (var entry in typesToProcess) + { + AddGenericArgumentDependencies.Execute(entry.TypeInstance.Type); + } + + // Phase 9: Interface and member-to-type dependencies + foreach (var entry in typesToProcess) + { + AddClassDependencies.Execute( + entry.TypeInstance.Type, + entry.Definition, + _domainResolver + ); + } + + // Phase 10: Backwards dependencies + foreach (var entry in typesToProcess) + { + AddBackwardsDependencies.Execute(entry.TypeInstance.Type); + } + + // Phase 11: Register types with their namespaces + AddTypesToNamespaces.Execute( + _typesToProcess.Values.Select(entry => entry.TypeInstance.Type) + ); + } } } diff --git a/ArchUnitNET/Loader/DomainResolver.cs b/ArchUnitNET/Loader/DomainResolver.cs index 838427edf..287d747ae 100644 --- a/ArchUnitNET/Loader/DomainResolver.cs +++ b/ArchUnitNET/Loader/DomainResolver.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Runtime.CompilerServices; using ArchUnitNET.Domain; -using ArchUnitNET.Loader.LoadTasks; using JetBrains.Annotations; using Mono.Cecil; using static ArchUnitNET.Domain.Visibility; @@ -13,10 +12,15 @@ namespace ArchUnitNET.Loader { + /// + /// Resolves Mono.Cecil type, method, and field references into cached domain objects. + /// Creates and deduplicates , , + /// , , and + /// instances. Passed into every load-task phase as the single source of truth for + /// domain object resolution. + /// internal class DomainResolver { - private readonly LoadTaskRegistry _loadTaskRegistry; - private readonly Dictionary _assemblies = new Dictionary(); @@ -32,19 +36,25 @@ internal class DomainResolver private readonly Dictionary _allFields = new Dictionary(); - public DomainResolver( - LoadTaskRegistry loadTaskRegistry - ) - { - _loadTaskRegistry = loadTaskRegistry; - } - + /// + /// All assemblies that have been created or cached. + /// public IEnumerable Assemblies => _assemblies.Values; + /// + /// All namespaces that have been created or cached. + /// public IEnumerable Namespaces => _namespaces.Values; + /// + /// All types that have been created or cached. + /// public IEnumerable> Types => _allTypes.Values; + /// + /// Returns the existing for the given full name, or creates + /// and caches a new one. + /// internal Assembly GetOrCreateAssembly( string assemblyName, string assemblyFullName, @@ -67,11 +77,10 @@ List assemblyReferences return assembly; } - internal bool ContainsAssembly(string assemblyFullName) - { - return _assemblies.ContainsKey(assemblyFullName); - } - + /// + /// Returns the existing for the given name, or creates + /// and caches a new one. + /// internal Namespace GetOrCreateNamespace(string namespaceName) { if (_namespaces.TryGetValue(namespaceName, out var existing)) @@ -84,20 +93,23 @@ internal Namespace GetOrCreateNamespace(string namespaceName) return ns; } - public IEnumerable GetAllNonCompilerGeneratedTypes() - { - return _allTypes - .Values.Select(instance => instance.Type) - .Distinct() - .Where(type => !type.IsCompilerGenerated); - } - + /// + /// Returns (or creates and caches) a type instance for the given type reference. + /// Used during type discovery in . + /// [NotNull] - internal IType GetOrCreateTypeFromTypeReference(TypeReference typeReference) + internal ITypeInstance GetOrCreateTypeInstanceFromTypeReference( + TypeReference typeReference + ) { - return GetOrCreateTypeInstanceFromTypeReference(typeReference, false).Type; + return GetOrCreateTypeInstanceFromTypeReference(typeReference, false); } + /// + /// Returns (or creates and caches) a stub type instance for the given type reference. + /// Used by load task phases when resolving dependency targets that may not have + /// been discovered during module loading. + /// [NotNull] internal ITypeInstance GetOrCreateStubTypeInstanceFromTypeReference( TypeReference typeReference @@ -144,23 +156,10 @@ bool isStub return GetOrCreateTypeInstanceFromTypeDefinition(typeDefinition, isStub); } - var resolvedTypeDefinition = ResolveTypeReferenceToTypeDefinition(typeReference); - if (resolvedTypeDefinition == null) - { - // When assemblies are loaded by path, there are cases where a dependent type cannot be resolved because - // the assembly dependency is not loaded in the current application domain. In this case, we create a - // stub type. - return GetOrCreateUnavailableTypeFromTypeReference(typeReference); - } - return GetOrCreateTypeInstanceFromTypeDefinition(resolvedTypeDefinition, isStub); - } - - private TypeDefinition ResolveTypeReferenceToTypeDefinition(TypeReference typeReference) - { - TypeDefinition typeDefinition; + TypeDefinition resolvedTypeDefinition; try { - typeDefinition = typeReference.Resolve(); + resolvedTypeDefinition = typeReference.Resolve(); } catch (AssemblyResolutionException e) { @@ -169,7 +168,15 @@ private TypeDefinition ResolveTypeReferenceToTypeDefinition(TypeReference typeRe e ); } - return typeDefinition; + + if (resolvedTypeDefinition == null) + { + // When assemblies are loaded by path, there are cases where a dependent type cannot be resolved because + // the assembly dependency is not loaded in the current application domain. In this case, we create a + // stub type. + return GetOrCreateUnavailableTypeFromTypeReference(typeReference); + } + return GetOrCreateTypeInstanceFromTypeDefinition(resolvedTypeDefinition, isStub); } private ITypeInstance GetOrCreateGenericParameterTypeInstanceFromTypeReference( @@ -311,9 +318,7 @@ bool isStub { declaringTypeReference = declaringTypeReference.DeclaringType; } - var currentNamespace = GetOrCreateNamespace( - declaringTypeReference.Namespace - ); + var currentNamespace = GetOrCreateNamespace(declaringTypeReference.Namespace); var currentAssembly = GetOrCreateAssembly( assemblyFullName, assemblyFullName, @@ -371,16 +376,6 @@ bool isStub ); } - if (!isStub && !isCompilerGenerated) - { - if (!typeDefinition.IsInterface) - { - LoadBaseTask(createdTypeInstance.Type, type, typeDefinition); - } - - LoadNonBaseTasks(createdTypeInstance, type, typeDefinition); - } - _allTypes.Add(assemblyQualifiedName, createdTypeInstance); var genericParameters = GetGenericParameters(typeDefinition); @@ -394,9 +389,10 @@ private ITypeInstance GetOrCreateUnavailableTypeFromTypeReferen TypeReference typeReference ) { + var typeReferenceFullName = typeReference.BuildFullName(); var assemblyQualifiedName = System.Reflection.Assembly.CreateQualifiedName( typeReference.Scope.Name, - typeReference.BuildFullName() + typeReferenceFullName ); if (_allTypes.TryGetValue(assemblyQualifiedName, out var existingTypeInstance)) { @@ -406,7 +402,7 @@ TypeReference typeReference var result = new TypeInstance( new UnavailableType( new Type( - typeReference.BuildFullName(), + typeReferenceFullName, typeReference.Name, GetOrCreateAssembly( typeReference.Scope.Name, @@ -498,6 +494,10 @@ FunctionPointerType functionPointerType return result; } + /// + /// Returns (or creates and caches) a for the + /// given method reference. Handles both generic and non-generic method references. + /// [NotNull] public MethodMemberInstance GetOrCreateMethodMemberFromMethodReference( [NotNull] ITypeInstance typeInstance, @@ -507,17 +507,22 @@ [NotNull] MethodReference methodReference var methodReferenceFullName = methodReference.BuildFullName(); if (methodReference.IsGenericInstance) { - if (_allMethods.TryGetValue(methodReferenceFullName, out var existingGenericInstance)) + if ( + _allMethods.TryGetValue( + methodReferenceFullName, + out var existingGenericInstance + ) + ) { return existingGenericInstance; } - var genericInstance = CreateGenericInstanceMethodMemberFromMethodReference( + var genericResult = CreateGenericInstanceMethodMemberFromMethodReference( typeInstance, methodReference ); - _allMethods.Add(methodReferenceFullName, genericInstance); - return genericInstance; + _allMethods.Add(methodReferenceFullName, genericResult); + return genericResult; } if (_allMethods.TryGetValue(methodReferenceFullName, out var existingMethodInstance)) @@ -526,7 +531,7 @@ [NotNull] MethodReference methodReference } var name = methodReference.BuildMethodMemberName(); - var fullName = methodReference.BuildFullName(); + var fullName = methodReferenceFullName; var isGeneric = methodReference.HasGenericParameters; var isCompilerGenerated = methodReference.IsCompilerGenerated(); MethodForm methodForm; @@ -622,6 +627,12 @@ MethodReference methodReference ); } + /// + /// Returns (or creates and caches) a for the given field + /// reference. When the reference is a , the field is + /// created with full fidelity (visibility, exact writability). Otherwise a stub with + /// default visibility () is created. + /// [NotNull] internal FieldMember GetOrCreateFieldMember( [NotNull] IType type, @@ -708,6 +719,10 @@ [NotNull] FieldDefinition fieldDefinition throw new ArgumentException("The field definition seems to have no visibility."); } + /// + /// Extracts generic parameters from a Cecil generic parameter provider (type or method) + /// and returns them as domain instances. + /// public IEnumerable GetGenericParameters( IGenericParameterProvider genericParameterProvider ) @@ -721,76 +736,9 @@ IGenericParameterProvider genericParameterProvider .Cast(); } - internal GenericArgument CreateGenericArgumentFromTypeReference(TypeReference typeReference) + private GenericArgument CreateGenericArgumentFromTypeReference(TypeReference typeReference) { return new GenericArgument(GetOrCreateStubTypeInstanceFromTypeReference(typeReference)); } - - private void LoadBaseTask(IType cls, Type type, TypeDefinition typeDefinition) - { - if (typeDefinition == null) - { - return; - } - - _loadTaskRegistry.Add( - typeof(AddBaseClassDependency), - new AddBaseClassDependency(cls, type, typeDefinition, this) - ); - } - - private void LoadNonBaseTasks( - ITypeInstance createdTypeInstance, - Type type, - TypeDefinition typeDefinition - ) - { - if (typeDefinition == null) - { - return; - } - - _loadTaskRegistry.Add( - typeof(AddMembers), - new AddMembers(createdTypeInstance, typeDefinition, this, type.Members) - ); - _loadTaskRegistry.Add( - typeof(AddGenericParameterDependencies), - new AddGenericParameterDependencies(type) - ); - _loadTaskRegistry.Add( - typeof(AddAttributesAndAttributeDependencies), - new AddAttributesAndAttributeDependencies( - createdTypeInstance.Type, - typeDefinition, - this - ) - ); - _loadTaskRegistry.Add( - typeof(AddFieldAndPropertyDependencies), - new AddFieldAndPropertyDependencies(createdTypeInstance.Type) - ); - _loadTaskRegistry.Add( - typeof(AddMethodDependencies), - new AddMethodDependencies(createdTypeInstance.Type, typeDefinition, this) - ); - _loadTaskRegistry.Add( - typeof(AddGenericArgumentDependencies), - new AddGenericArgumentDependencies(type) - ); - _loadTaskRegistry.Add( - typeof(AddClassDependencies), - new AddClassDependencies( - createdTypeInstance.Type, - typeDefinition, - this, - type.Dependencies - ) - ); - _loadTaskRegistry.Add( - typeof(AddBackwardsDependencies), - new AddBackwardsDependencies(createdTypeInstance.Type) - ); - } } } diff --git a/ArchUnitNET/Loader/LoadTaskRegistry.cs b/ArchUnitNET/Loader/LoadTaskRegistry.cs deleted file mode 100644 index 9c7f1939b..000000000 --- a/ArchUnitNET/Loader/LoadTaskRegistry.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ArchUnitNET.Domain.Extensions; -using ArchUnitNET.Loader.LoadTasks; - -namespace ArchUnitNET.Loader -{ - internal class LoadTaskRegistry - { - private readonly Dictionary> _tasksTodo = - new Dictionary>(); - - public void ExecuteTasks(List taskOrder) - { - var comparer = new TaskComparer(taskOrder); - - ICollection tasksTodoKeys = _tasksTodo.Keys; - - var prioritizedTodoKeys = tasksTodoKeys.OrderBy(taskType => taskType, comparer); - prioritizedTodoKeys.ForEach(taskKey => - { - if (!_tasksTodo.TryGetValue(taskKey, out var queueOfTasks)) - { - return; - } - - while (queueOfTasks.Count > 0) - { - var nextTask = queueOfTasks.Peek(); - nextTask.Execute(); - queueOfTasks.Dequeue(); - } - }); - } - - public void Add(System.Type taskType, ILoadTask task) - { - if (taskType == null) - { - throw new ArgumentNullException(nameof(taskType)); - } - - if (task == null) - { - throw new ArgumentNullException(nameof(task)); - } - - var taskQueueExists = _tasksTodo.TryGetValue(taskType, out var queueOfTasksOfType); - if (!taskQueueExists) - { - queueOfTasksOfType = new Queue(); - } - - queueOfTasksOfType.Enqueue(task); - _tasksTodo[taskType] = queueOfTasksOfType; - } - - private class TaskComparer : IComparer - { - private readonly List _taskOrder; - - public TaskComparer(List taskOrder) - { - _taskOrder = taskOrder; - } - - public int Compare(System.Type x, System.Type y) - { - var a = _taskOrder.IndexOf(x); - var b = _taskOrder.IndexOf(y); - - if (a > b) - { - return 1; - } - - if (a < b) - { - return -1; - } - - return 0; - } - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs index 2fbbcff6f..60ef7e904 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs @@ -1,9 +1,3 @@ -// Copyright 2019 Florian Gather -// Copyright 2019 Paula Ruiz -// Copyright 2019 Fritz Brandhuber -// -// SPDX-License-Identifier: Apache-2.0 - using System.Collections.Generic; using System.Linq; using ArchUnitNET.Domain; @@ -14,120 +8,169 @@ namespace ArchUnitNET.Loader.LoadTasks { - internal class AddAttributesAndAttributeDependencies : ILoadTask + /// + /// Creates attribute instances from Cecil custom attributes on the type, its generic + /// parameters, and its members, then adds corresponding attribute-type dependencies. + /// + internal static class AddAttributesAndAttributeDependencies { - private readonly IType _type; - private readonly TypeDefinition _typeDefinition; - private readonly DomainResolver _domainResolver; - - public AddAttributesAndAttributeDependencies( + internal static void Execute( IType type, TypeDefinition typeDefinition, - DomainResolver domainResolver + DomainResolver domainResolver, + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs ) { - _type = type; - _typeDefinition = typeDefinition; - _domainResolver = domainResolver; - } - - public void Execute() - { - _typeDefinition.CustomAttributes.ForEach( - AddAttributeArgumentReferenceDependenciesToOriginType + typeDefinition.CustomAttributes.ForEach(attr => + AddAttributeArgumentReferenceDependencies(type, attr, domainResolver) ); var typeAttributeInstances = CreateAttributesFromCustomAttributes( - _typeDefinition.CustomAttributes + typeDefinition.CustomAttributes, + domainResolver ) .ToList(); - _type.AttributeInstances.AddRange(typeAttributeInstances); + type.AttributeInstances.AddRange(typeAttributeInstances); var typeAttributeDependencies = typeAttributeInstances.Select( - attributeInstance => new AttributeTypeDependency(_type, attributeInstance) + attributeInstance => new AttributeTypeDependency(type, attributeInstance) ); - _type.Dependencies.AddRange(typeAttributeDependencies); - SetUpAttributesForTypeGenericParameters(); - CollectAttributesForMembers(); + type.Dependencies.AddRange(typeAttributeDependencies); + + SetUpAttributesForTypeGenericParameters(type, typeDefinition, domainResolver); + CollectAttributesForMembers(type, typeDefinition, domainResolver, methodPairs); } - private void SetUpAttributesForTypeGenericParameters() + private static void SetUpAttributesForTypeGenericParameters( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver + ) { - foreach (var genericParameter in _typeDefinition.GenericParameters) + foreach (var genericParameter in typeDefinition.GenericParameters) { - var param = _type.GenericParameters.First(parameter => + var param = type.GenericParameters.First(parameter => parameter.Name == genericParameter.Name ); var attributeInstances = CreateAttributesFromCustomAttributes( - genericParameter.CustomAttributes + genericParameter.CustomAttributes, + domainResolver ) .ToList(); - _type.AttributeInstances.AddRange(attributeInstances); + type.AttributeInstances.AddRange(attributeInstances); param.AttributeInstances.AddRange(attributeInstances); var genericParameterAttributeDependencies = attributeInstances.Select( - attributeInstance => new AttributeTypeDependency(_type, attributeInstance) + attributeInstance => new AttributeTypeDependency(type, attributeInstance) ); - _type.Dependencies.AddRange(genericParameterAttributeDependencies); + type.Dependencies.AddRange(genericParameterAttributeDependencies); } } - private void CollectAttributesForMembers() + private static void CollectAttributesForMembers( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver, + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs + ) { - _typeDefinition + typeDefinition .Fields.Where(x => !x.IsBackingField() && !x.IsCompilerGenerated()) - .ForEach(SetUpAttributesForFields); + .ForEach(fieldDef => SetUpAttributesForField(type, fieldDef, domainResolver)); - _typeDefinition + typeDefinition .Properties.Where(x => !x.IsCompilerGenerated()) - .ForEach(SetUpAttributesForProperties); + .ForEach(propDef => SetUpAttributesForProperty(type, propDef, domainResolver)); - _typeDefinition + // Build a lookup from method full-name -> MethodMember to avoid O(n) scans + var methodMemberByFullName = new Dictionary(methodPairs.Count); + foreach (var pair in methodPairs) + { + var key = pair.Definition.BuildFullName(); + if (!methodMemberByFullName.ContainsKey(key)) + { + methodMemberByFullName[key] = pair.Member; + } + } + + typeDefinition .Methods.Where(x => !x.IsCompilerGenerated()) - .ForEach(SetUpAttributesForMethods); + .ForEach(methodDef => + { + MethodMember methodMember; + if ( + !methodMemberByFullName.TryGetValue( + methodDef.BuildFullName(), + out methodMember + ) + ) + { + return; + } + + SetUpAttributesForMethod(methodDef, methodMember, type, domainResolver); + }); } - private void SetUpAttributesForFields(FieldDefinition fieldDefinition) + private static void SetUpAttributesForField( + IType type, + FieldDefinition fieldDefinition, + DomainResolver domainResolver + ) { - var fieldMember = _type - .GetFieldMembers() + var fieldMember = type.GetFieldMembers() .WhereFullNameIs(fieldDefinition.FullName) .RequiredNotNull(); CollectMemberAttributesAndDependencies( + type, fieldMember, fieldDefinition.CustomAttributes.ToList(), - fieldMember.MemberDependencies + fieldMember.MemberDependencies, + domainResolver ); } - private void SetUpAttributesForProperties(PropertyDefinition propertyDefinition) + private static void SetUpAttributesForProperty( + IType type, + PropertyDefinition propertyDefinition, + DomainResolver domainResolver + ) { - var propertyMember = _type - .GetPropertyMembers() + var propertyMember = type.GetPropertyMembers() .WhereFullNameIs(propertyDefinition.FullName) .RequiredNotNull(); CollectMemberAttributesAndDependencies( + type, propertyMember, propertyDefinition.CustomAttributes.ToList(), - propertyMember.AttributeDependencies + propertyMember.AttributeDependencies, + domainResolver ); } - private void SetUpAttributesForMethods(MethodDefinition methodDefinition) + private static void SetUpAttributesForMethod( + MethodDefinition methodDefinition, + MethodMember methodMember, + IType type, + DomainResolver domainResolver + ) { - var methodMember = _type - .GetMethodMembers() - .WhereFullNameIs(methodDefinition.BuildFullName()) - .RequiredNotNull(); var memberCustomAttributes = methodDefinition.GetAllMethodCustomAttributes().ToList(); - SetUpAttributesForMethodGenericParameters(methodDefinition, methodMember); + SetUpAttributesForMethodGenericParameters( + methodDefinition, + methodMember, + domainResolver + ); CollectMemberAttributesAndDependencies( + type, methodMember, memberCustomAttributes, - methodMember.MemberDependencies + methodMember.MemberDependencies, + domainResolver ); } - private void SetUpAttributesForMethodGenericParameters( + private static void SetUpAttributesForMethodGenericParameters( MethodDefinition methodDefinition, - MethodMember methodMember + MethodMember methodMember, + DomainResolver domainResolver ) { foreach (var genericParameter in methodDefinition.GenericParameters) @@ -136,8 +179,17 @@ MethodMember methodMember parameter.Name == genericParameter.Name ); var customAttributes = genericParameter.CustomAttributes; - customAttributes.ForEach(AddAttributeArgumentReferenceDependenciesToOriginType); - var attributeInstances = CreateAttributesFromCustomAttributes(customAttributes) + customAttributes.ForEach(attr => + AddAttributeArgumentReferenceDependencies( + methodMember.DeclaringType, + attr, + domainResolver + ) + ); + var attributeInstances = CreateAttributesFromCustomAttributes( + customAttributes, + domainResolver + ) .ToList(); methodMember.AttributeInstances.AddRange(attributeInstances); param.AttributeInstances.AddRange(attributeInstances); @@ -151,28 +203,33 @@ MethodMember methodMember } } - private void CollectMemberAttributesAndDependencies( - IMember methodMember, + private static void CollectMemberAttributesAndDependencies( + IType type, + IMember member, List memberCustomAttributes, - List attributeDependencies + List attributeDependencies, + DomainResolver domainResolver ) { - memberCustomAttributes.ForEach(AddAttributeArgumentReferenceDependenciesToOriginType); + memberCustomAttributes.ForEach(attr => + AddAttributeArgumentReferenceDependencies(type, attr, domainResolver) + ); var memberAttributeInstances = CreateAttributesFromCustomAttributes( - memberCustomAttributes + memberCustomAttributes, + domainResolver ) .ToList(); - methodMember.AttributeInstances.AddRange(memberAttributeInstances); - var methodAttributeDependencies = CreateMemberAttributeDependencies( - methodMember, - memberAttributeInstances + member.AttributeInstances.AddRange(memberAttributeInstances); + var methodAttributeDependencies = memberAttributeInstances.Select( + attributeInstance => new AttributeMemberDependency(member, attributeInstance) ); attributeDependencies.AddRange(methodAttributeDependencies); } [NotNull] - public IEnumerable CreateAttributesFromCustomAttributes( - IEnumerable customAttributes + private static IEnumerable CreateAttributesFromCustomAttributes( + IEnumerable customAttributes, + DomainResolver domainResolver ) { return customAttributes @@ -184,23 +241,13 @@ IEnumerable customAttributes && customAttribute.AttributeType.FullName != "System.Runtime.CompilerServices.NullableContextAttribute" ) - .Select(attr => attr.CreateAttributeFromCustomAttribute(_domainResolver)); + .Select(attr => attr.CreateAttributeFromCustomAttribute(domainResolver)); } - [NotNull] - private static IEnumerable CreateMemberAttributeDependencies( - IMember member, - IEnumerable attributes - ) - { - return attributes.Select(attributeInstance => new AttributeMemberDependency( - member, - attributeInstance - )); - } - - private void AddAttributeArgumentReferenceDependenciesToOriginType( - ICustomAttribute customAttribute + private static void AddAttributeArgumentReferenceDependencies( + IType type, + ICustomAttribute customAttribute, + DomainResolver domainResolver ) { if (!customAttribute.HasConstructorArguments) @@ -219,11 +266,11 @@ attributeArgument.Value is TypeReference typeReference ) .ForEach(tuple => { - var argumentType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + var argumentType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( tuple.typeReference ); - var dependency = new TypeReferenceDependency(_type, argumentType); - _type.Dependencies.Add(dependency); + var dependency = new TypeReferenceDependency(type, argumentType); + type.Dependencies.Add(dependency); }); } } diff --git a/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs index a50b51fc3..409b45356 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs @@ -5,22 +5,20 @@ namespace ArchUnitNET.Loader.LoadTasks { - internal class AddBackwardsDependencies : ILoadTask + /// + /// Registers each of the type's dependencies as a backwards dependency on the + /// target type, and each member-member dependency as a backwards dependency on + /// the target member. + /// + internal static class AddBackwardsDependencies { - private readonly IType _type; - - public AddBackwardsDependencies(IType type) - { - _type = type; - } - - public void Execute() + internal static void Execute(IType type) { - _type.Dependencies.ForEach(dependency => + type.Dependencies.ForEach(dependency => dependency.Target.BackwardsDependencies.Add(dependency) ); - var memberMemberDependencies = _type + var memberMemberDependencies = type .Members.SelectMany(member => member.MemberDependencies) .OfType(); memberMemberDependencies.ForEach(memberDependency => diff --git a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs b/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs index 8ab9a7198..2f4e11b81 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs @@ -4,52 +4,40 @@ namespace ArchUnitNET.Loader.LoadTasks { - internal class AddBaseClassDependency : ILoadTask + /// + /// If the type has a base class, creates an + /// and adds it to the type's dependency list. Skipped for interfaces and types + /// whose base is not a . + /// + internal static class AddBaseClassDependency { - private readonly IType _cls; - private readonly Type _type; - private readonly TypeDefinition _typeDefinition; - private readonly DomainResolver _domainResolver; - - public AddBaseClassDependency( - IType cls, - Type type, + internal static void Execute( + IType type, TypeDefinition typeDefinition, DomainResolver domainResolver ) { - _cls = cls; - _type = type; - _typeDefinition = typeDefinition; - _domainResolver = domainResolver; - } - - public void Execute() - { - var typeDefinitionBaseType = _typeDefinition?.BaseType; - - if (typeDefinitionBaseType == null) + var baseTypeRef = typeDefinition.BaseType; + if (baseTypeRef == null) { return; } - var baseType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - typeDefinitionBaseType - ); + var baseType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(baseTypeRef); if (!(baseType.Type is Class baseClass)) { return; } var dependency = new InheritsBaseClassDependency( - _cls, + type, new TypeInstance( baseClass, baseType.GenericArguments, baseType.ArrayDimensions ) ); - _type.Dependencies.Add(dependency); + type.Dependencies.Add(dependency); } } } diff --git a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs index c4ab6b127..4b0b5af96 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs @@ -7,50 +7,34 @@ namespace ArchUnitNET.Loader.LoadTasks { - internal class AddClassDependencies : ILoadTask + /// + /// Creates for each interface the type + /// implements (including inherited interfaces), then rolls up all member-level + /// dependencies to the type's dependency list. + /// + internal static class AddClassDependencies { - private readonly List _dependencies; - private readonly IType _type; - private readonly TypeDefinition _typeDefinition; - private readonly DomainResolver _domainResolver; - - public AddClassDependencies( + internal static void Execute( IType type, TypeDefinition typeDefinition, - DomainResolver domainResolver, - List dependencies + DomainResolver domainResolver ) { - _type = type; - _typeDefinition = typeDefinition; - _domainResolver = domainResolver; - _dependencies = dependencies; - } - - public void Execute() - { - AddInterfaceDependencies(); - AddMemberDependencies(); - } - - private void AddMemberDependencies() - { - _type.Members.ForEach(member => - { - _dependencies.AddRange(member.Dependencies); - }); - } - - private void AddInterfaceDependencies() - { - GetInterfacesImplementedByClass(_typeDefinition) + // Interface dependencies + GetInterfacesImplementedByClass(typeDefinition) .ForEach(target => { - var targetType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + var targetType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( target ); - _dependencies.Add(new ImplementsInterfaceDependency(_type, targetType)); + type.Dependencies.Add(new ImplementsInterfaceDependency(type, targetType)); }); + + // Member dependencies rolled up to type level + type.Members.ForEach(member => + { + type.Dependencies.AddRange(member.Dependencies); + }); } private static IEnumerable GetInterfacesImplementedByClass( diff --git a/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs index 041c072ba..bec50b4f8 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs @@ -4,40 +4,33 @@ namespace ArchUnitNET.Loader.LoadTasks { - internal class AddFieldAndPropertyDependencies : ILoadTask + /// + /// Creates and + /// instances for each field and property member of the type. + /// + internal static class AddFieldAndPropertyDependencies { - private readonly IType _type; - - public AddFieldAndPropertyDependencies(IType type) - { - _type = type; - } - - public void Execute() + internal static void Execute(IType type) { - _type - .GetFieldMembers() + type.GetFieldMembers() .ForEach(field => { var dependency = new FieldTypeDependency(field); - AddDependencyIfMissing(field, dependency); + if (!field.MemberDependencies.Contains(dependency)) + { + field.MemberDependencies.Add(dependency); + } }); - _type - .GetPropertyMembers() + type.GetPropertyMembers() .ForEach(property => { var dependency = new PropertyTypeDependency(property); - AddDependencyIfMissing(property, dependency); + if (!property.MemberDependencies.Contains(dependency)) + { + property.MemberDependencies.Add(dependency); + } }); } - - private static void AddDependencyIfMissing(IMember member, IMemberTypeDependency dependency) - { - if (!member.MemberDependencies.Contains(dependency)) - { - member.MemberDependencies.Add(dependency); - } - } } } diff --git a/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs index c109be34b..1a5ac5b0b 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs @@ -1,38 +1,51 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using ArchUnitNET.Domain; using ArchUnitNET.Domain.Dependencies; -using JetBrains.Annotations; namespace ArchUnitNET.Loader.LoadTasks { - public class AddGenericArgumentDependencies : ILoadTask + /// + /// Propagates generic-parameter dependencies to the owning type and member, + /// then recursively discovers nested generic-argument dependencies in all + /// existing type and member dependencies. + /// + internal static class AddGenericArgumentDependencies { - [NotNull] - private readonly IType _type; - - public AddGenericArgumentDependencies([NotNull] IType type) + internal static void Execute(IType type) { - _type = type; - } + // Type-level generic argument dependencies + foreach (var parameter in type.GenericParameters) + { + type.Dependencies.AddRange(parameter.Dependencies); + } - public void Execute() - { - AddTypeGenericArgumentDependencies(); - AddMemberGenericArgumentDependencies(); + // Member-level generic argument dependencies + foreach (var member in type.Members) + { + foreach (var parameter in member.GenericParameters) + { + member.MemberDependencies.AddRange( + parameter.Dependencies.Cast() + ); + } + } + // Recursive generic arguments in type dependencies var typeDependencies = new List(); - foreach (var dependency in _type.Dependencies) + foreach (var dependency in type.Dependencies) { FindGenericArgumentsInTypeDependenciesRecursive( + type, dependency.TargetGenericArguments, typeDependencies ); } - _type.Dependencies.AddRange(typeDependencies); + type.Dependencies.AddRange(typeDependencies); - foreach (var member in _type.Members) + // Recursive generic arguments in member dependencies + foreach (var member in type.Members) { var memberDependencies = new List(); foreach (var dependency in member.Dependencies) @@ -48,15 +61,8 @@ public void Execute() } } - private void AddTypeGenericArgumentDependencies() - { - foreach (var parameter in _type.GenericParameters) - { - _type.Dependencies.AddRange(parameter.Dependencies); - } - } - - private void FindGenericArgumentsInTypeDependenciesRecursive( + private static void FindGenericArgumentsInTypeDependenciesRecursive( + IType type, IEnumerable targetGenericArguments, ICollection createdDependencies ) @@ -67,27 +73,15 @@ var genericArgument in targetGenericArguments.Where(argument => ) ) { - createdDependencies.Add(new GenericArgumentTypeDependency(_type, genericArgument)); + createdDependencies.Add(new GenericArgumentTypeDependency(type, genericArgument)); FindGenericArgumentsInTypeDependenciesRecursive( + type, genericArgument.GenericArguments, createdDependencies ); } } - private void AddMemberGenericArgumentDependencies() - { - foreach (var member in _type.Members) - { - foreach (var parameter in member.GenericParameters) - { - member.MemberDependencies.AddRange( - parameter.Dependencies.Cast() - ); - } - } - } - private static void FindGenericArgumentsInMemberDependenciesRecursive( IMember member, IEnumerable targetGenericArguments, diff --git a/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs index fc9ed1ae4..3cedef396 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs @@ -1,30 +1,20 @@ -using ArchUnitNET.Domain; +using ArchUnitNET.Domain; using ArchUnitNET.Domain.Dependencies; -using JetBrains.Annotations; namespace ArchUnitNET.Loader.LoadTasks { - public class AddGenericParameterDependencies : ILoadTask + /// + /// Assigns declarers to generic parameters and creates type-constraint dependencies + /// for both type-level and member-level generic parameters. + /// + internal static class AddGenericParameterDependencies { - [NotNull] - private readonly IType _type; - - public AddGenericParameterDependencies([NotNull] IType type) + internal static void Execute(IType type) { - _type = type; - } - - public void Execute() - { - AddTypeGenericParameterDependencies(); - AddMemberGenericParameterDependencies(); - } - - private void AddTypeGenericParameterDependencies() - { - foreach (var genericParameter in _type.GenericParameters) + // Type-level generic parameters + foreach (var genericParameter in type.GenericParameters) { - genericParameter.AssignDeclarer(_type); + genericParameter.AssignDeclarer(type); foreach (var typeInstanceConstraint in genericParameter.TypeInstanceConstraints) { var dependency = new TypeGenericParameterTypeConstraintDependency( @@ -34,11 +24,9 @@ private void AddTypeGenericParameterDependencies() genericParameter.Dependencies.Add(dependency); } } - } - private void AddMemberGenericParameterDependencies() - { - foreach (var member in _type.Members) + // Member-level generic parameters + foreach (var member in type.Members) { foreach (var genericParameter in member.GenericParameters) { diff --git a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs index c9b887f92..b65160913 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs @@ -1,76 +1,104 @@ using System.Collections.Generic; using System.Linq; using ArchUnitNET.Domain; +using ArchUnitNET.Domain.Extensions; using JetBrains.Annotations; using Mono.Cecil; namespace ArchUnitNET.Loader.LoadTasks { - internal class AddMembers : ILoadTask + /// + /// Creates field, property, and method members from the Cecil + /// and adds them to the domain type. + /// Returns a containing method pairs and + /// a mapping from getter/setter s to + /// their s. + /// + internal static class AddMembers { - private readonly MemberList _memberList; - private readonly ITypeInstance _typeInstance; - private readonly TypeDefinition _typeDefinition; - private readonly DomainResolver _domainResolver; - - public AddMembers( + internal static MemberData Execute( ITypeInstance typeInstance, TypeDefinition typeDefinition, - DomainResolver domainResolver, - MemberList memberList + DomainResolver domainResolver ) { - _typeInstance = typeInstance; - _typeDefinition = typeDefinition; - _domainResolver = domainResolver; - _memberList = memberList; - } - - public void Execute() - { - var members = CreateMembers(_typeDefinition); - _memberList.AddRange(members); + var methodPairs = new List<(MethodMember Member, MethodDefinition Definition)>(); + var propertyByAccessor = new Dictionary(); + var members = CreateMembers( + typeInstance, + typeDefinition, + domainResolver, + methodPairs, + propertyByAccessor + ); + typeInstance.Type.Members.AddRange(members); + return new MemberData(methodPairs, propertyByAccessor); } [NotNull] - private IEnumerable CreateMembers([NotNull] TypeDefinition typeDefinition) + private static IEnumerable CreateMembers( + ITypeInstance typeInstance, + [NotNull] TypeDefinition typeDefinition, + DomainResolver domainResolver, + List<(MethodMember Member, MethodDefinition Definition)> methodPairs, + Dictionary propertyByAccessor + ) { - return typeDefinition + var fieldMembers = typeDefinition .Fields.Where(fieldDefinition => !fieldDefinition.IsBackingField()) .Select(fieldDef => - (IMember)_domainResolver.GetOrCreateFieldMember(_typeInstance.Type, fieldDef) - ) - .Concat( - typeDefinition - .Properties.Select(CreatePropertyMember) - .Concat( - typeDefinition.Methods.Select(method => - _domainResolver - .GetOrCreateMethodMemberFromMethodReference( - _typeInstance, - method - ) - .Member - ) - ) - ) + (IMember)domainResolver.GetOrCreateFieldMember(typeInstance.Type, fieldDef) + ); + + var propertyMembers = typeDefinition.Properties.Select(propDef => + { + var propertyMember = CreatePropertyMember(typeInstance, propDef, domainResolver); + if (propDef.GetMethod != null) + { + propertyByAccessor[propDef.GetMethod] = propertyMember; + } + + if (propDef.SetMethod != null) + { + propertyByAccessor[propDef.SetMethod] = propertyMember; + } + + return (IMember)propertyMember; + }); + + var methodMembers = typeDefinition.Methods.Select(method => + { + var member = domainResolver + .GetOrCreateMethodMemberFromMethodReference(typeInstance, method) + .Member; + methodPairs.Add((member, method)); + return (IMember)member; + }); + + return fieldMembers + .Concat(propertyMembers) + .Concat(methodMembers) .Where(member => !member.IsCompilerGenerated); } [NotNull] - private IMember CreatePropertyMember(PropertyDefinition propertyDefinition) + private static PropertyMember CreatePropertyMember( + ITypeInstance typeInstance, + PropertyDefinition propertyDefinition, + DomainResolver domainResolver + ) { var typeReference = propertyDefinition.PropertyType; - var propertyType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + var propertyType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( typeReference ); var isCompilerGenerated = propertyDefinition.IsCompilerGenerated(); var isStatic = (propertyDefinition.SetMethod != null && propertyDefinition.SetMethod.IsStatic) || (propertyDefinition.GetMethod != null && propertyDefinition.GetMethod.IsStatic); - var writeAccessor = GetWriteAccessor(propertyDefinition); + var writeAccessor = GetPropertyWriteAccessor(propertyDefinition); return new PropertyMember( - _typeInstance.Type, + typeInstance.Type, propertyDefinition.Name, propertyDefinition.FullName, propertyType, @@ -80,19 +108,21 @@ private IMember CreatePropertyMember(PropertyDefinition propertyDefinition) ); } - private static Writability GetWriteAccessor([NotNull] PropertyDefinition propertyDefinition) + private static Writability GetPropertyWriteAccessor( + [NotNull] PropertyDefinition propertyDefinition + ) { - bool isReadOnly = propertyDefinition.SetMethod == null; - - if (isReadOnly) + if (propertyDefinition.SetMethod == null) { return Writability.ReadOnly; } - bool isInitSetter = CheckPropertyHasInitSetterInNetStandardCompatibleWay( - propertyDefinition - ); - return isInitSetter ? Writability.InitOnly : Writability.Writable; + if (CheckPropertyHasInitSetterInNetStandardCompatibleWay(propertyDefinition)) + { + return Writability.InitOnly; + } + + return Writability.Writable; } private static bool CheckPropertyHasInitSetterInNetStandardCompatibleWay( @@ -105,4 +135,26 @@ PropertyDefinition propertyDefinition .FullName == "System.Runtime.CompilerServices.IsExternalInit"; } } + + /// + /// Bundles the results of member creation: method (MethodMember, MethodDefinition) pairs + /// and a mapping from getter/setter MethodDefinitions to their PropertyMembers. + /// + internal sealed class MemberData + { + public MemberData( + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs, + IReadOnlyDictionary propertyByAccessor + ) + { + MethodPairs = methodPairs; + PropertyByAccessor = propertyByAccessor; + } + + public IReadOnlyList<( + MethodMember Member, + MethodDefinition Definition + )> MethodPairs { get; } + public IReadOnlyDictionary PropertyByAccessor { get; } + } } diff --git a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs index 03d1948a5..a1c37745f 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs @@ -12,50 +12,47 @@ namespace ArchUnitNET.Loader.LoadTasks { - internal class AddMethodDependencies : ILoadTask + /// + /// Resolves method signature and body dependencies for each method in the type, + /// including method calls, body types, cast types, type checks, metadata types, + /// and accessed fields. Also links getter/setter methods to their properties. + /// + internal static class AddMethodDependencies { - private readonly IType _type; - private readonly TypeDefinition _typeDefinition; - private readonly DomainResolver _domainResolver; - - public AddMethodDependencies( + internal static void Execute( IType type, - TypeDefinition typeDefinition, - DomainResolver domainResolver + DomainResolver domainResolver, + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs, + IReadOnlyDictionary propertyByAccessor ) { - _type = type; - _typeDefinition = typeDefinition; - _domainResolver = domainResolver; - } - - public void Execute() - { - _typeDefinition - .Methods.Where(methodDefinition => - _type.GetMemberWithFullName(methodDefinition.BuildFullName()) is MethodMember - ) - .Select(definition => - ( - methodMember: _type.GetMemberWithFullName(definition.BuildFullName()) - as MethodMember, - methodDefinition: definition - ) - ) - .Select(tuple => + methodPairs + .Where(pair => pair.Member != null && !pair.Member.IsCompilerGenerated) + .Select(pair => { - var (methodMember, methodDefinition) = tuple; var dependencies = CreateMethodSignatureDependencies( - methodDefinition, - methodMember + pair.Definition, + pair.Member, + domainResolver ) - .Concat(CreateMethodBodyDependencies(methodDefinition, methodMember)); - if (methodDefinition.IsSetter || methodDefinition.IsGetter) + .Concat( + CreateMethodBodyDependencies( + type, + pair.Definition, + pair.Member, + domainResolver + ) + ); + if (pair.Definition.IsSetter || pair.Definition.IsGetter) { - AssignDependenciesToProperty(methodMember, methodDefinition); + AssignDependenciesToProperty( + pair.Member, + pair.Definition, + propertyByAccessor + ); } - return (methodMember, dependencies); + return (pair.Member, dependencies); }) .ForEach(tuple => { @@ -64,27 +61,20 @@ public void Execute() }); } - private void AssignDependenciesToProperty( + private static void AssignDependenciesToProperty( MethodMember methodMember, - MethodDefinition methodDefinition + MethodDefinition methodDefinition, + IReadOnlyDictionary propertyByAccessor ) { - var methodForm = methodDefinition.GetMethodForm(); - var matchFunction = GetMatchFunction(methodForm); - matchFunction.RequiredNotNull(); - - var accessedProperty = MatchToPropertyMember( - methodMember.Name, - methodMember.FullName, - matchFunction - ); - if (accessedProperty == null) + if (!propertyByAccessor.TryGetValue(methodDefinition, out var accessedProperty)) { return; } - accessedProperty.IsVirtual = accessedProperty.IsVirtual || methodMember.IsVirtual; + accessedProperty.IsVirtual |= methodMember.IsVirtual; + var methodForm = methodDefinition.GetMethodForm(); switch (methodForm) { case MethodForm.Getter: @@ -114,15 +104,16 @@ MethodDefinition methodDefinition } [NotNull] - private IEnumerable CreateMethodSignatureDependencies( + private static IEnumerable CreateMethodSignatureDependencies( MethodReference methodReference, - MethodMember methodMember + MethodMember methodMember, + DomainResolver domainResolver ) { - var returnType = methodReference.GetReturnType(_domainResolver); + var returnType = methodReference.GetReturnType(domainResolver); return (returnType != null ? new[] { returnType } : Array.Empty>()) - .Concat(methodReference.GetParameters(_domainResolver)) - .Concat(methodReference.GetGenericParameters(_domainResolver)) + .Concat(methodReference.GetParameters(domainResolver)) + .Concat(methodReference.GetGenericParameters(domainResolver)) .Distinct() .Select(signatureType => new MethodSignatureDependency( methodMember, @@ -131,9 +122,11 @@ MethodMember methodMember } [NotNull] - private IEnumerable CreateMethodBodyDependencies( + private static IEnumerable CreateMethodBodyDependencies( + IType type, MethodDefinition methodDefinition, - MethodMember methodMember + MethodMember methodMember, + DomainResolver domainResolver ) { var methodBody = methodDefinition.Body; @@ -151,7 +144,8 @@ MethodMember methodMember out methodDefinition, ref methodBody, bodyTypes, - visitedMethodReferences + visitedMethodReferences, + domainResolver ); } @@ -161,11 +155,12 @@ MethodMember methodMember out methodDefinition, ref methodBody, bodyTypes, - visitedMethodReferences + visitedMethodReferences, + domainResolver ); } - var scan = methodDefinition.ScanMethodBody(_domainResolver); + var scan = methodDefinition.ScanMethodBody(domainResolver); bodyTypes.AddRange(scan.BodyTypes); var castTypes = scan.CastTypes; @@ -183,7 +178,8 @@ MethodMember methodMember castTypes, typeCheckTypes, metaDataTypes, - accessedFieldMembers + accessedFieldMembers, + domainResolver ); foreach ( @@ -241,14 +237,15 @@ var fieldMember in accessedFieldMembers } } - private IEnumerable CreateMethodBodyDependenciesRecursive( + private static IEnumerable CreateMethodBodyDependenciesRecursive( MethodBody methodBody, ICollection visitedMethodReferences, List> bodyTypes, List> castTypes, List> typeCheckTypes, List> metaDataTypes, - List accessedFieldMembers + List accessedFieldMembers, + DomainResolver domainResolver ) { var calledMethodReferences = methodBody @@ -261,10 +258,10 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc { visitedMethodReferences.Add(calledMethodReference); - var calledType = _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + var calledType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( calledMethodReference.DeclaringType ); - var calledMethodMember = _domainResolver.GetOrCreateMethodMemberFromMethodReference( + var calledMethodMember = domainResolver.GetOrCreateMethodMemberFromMethodReference( calledType, calledMethodReference ); @@ -297,11 +294,12 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc out calledMethodDefinition, ref calledMethodBody, bodyTypes, - visitedMethodReferences + visitedMethodReferences, + domainResolver ); } - var calledScan = calledMethodDefinition.ScanMethodBody(_domainResolver); + var calledScan = calledMethodDefinition.ScanMethodBody(domainResolver); bodyTypes.AddRange(calledScan.BodyTypes); castTypes.AddRange(calledScan.CastTypes); typeCheckTypes.AddRange(calledScan.TypeCheckTypes); @@ -316,7 +314,8 @@ var dep in CreateMethodBodyDependenciesRecursive( castTypes, typeCheckTypes, metaDataTypes, - accessedFieldMembers + accessedFieldMembers, + domainResolver ) ) { @@ -330,11 +329,12 @@ var dep in CreateMethodBodyDependenciesRecursive( } } - private void HandleIterator( + private static void HandleIterator( out MethodDefinition methodDefinition, ref MethodBody methodBody, List> bodyTypes, - ICollection visitedMethodReferences + ICollection visitedMethodReferences, + DomainResolver domainResolver ) { var compilerGeneratedGeneratorObject = ( @@ -358,16 +358,17 @@ ICollection visitedMethodReferences bodyTypes.AddRange( fieldsExceptGeneratorStateInfo.Select(bodyField => - _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) ) ); } - private void HandleAsync( + private static void HandleAsync( out MethodDefinition methodDefinition, ref MethodBody methodBody, List> bodyTypes, - ICollection visitedMethodReferences + ICollection visitedMethodReferences, + DomainResolver domainResolver ) { var compilerGeneratedGeneratorObject = ( @@ -400,76 +401,9 @@ ICollection visitedMethodReferences bodyTypes.AddRange( fieldsExceptGeneratorStateInfo.Select(bodyField => - _domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) ) ); } - - private static MatchFunction GetMatchFunction(MethodForm methodForm) - { - MatchFunction matchFunction; - switch (methodForm) - { - case MethodForm.Getter: - matchFunction = new MatchFunction(RegexUtils.MatchGetPropertyName); - break; - case MethodForm.Setter: - matchFunction = new MatchFunction(RegexUtils.MatchSetPropertyName); - break; - default: - matchFunction = null; - break; - } - - return matchFunction.RequiredNotNull(); - } - - private PropertyMember MatchToPropertyMember( - string name, - string fullName, - MatchFunction matchFunction - ) - { - try - { - var accessedMemberName = matchFunction.MatchNameFunction(name); - if (accessedMemberName != null) - { - var foundNameMatches = _type - .GetPropertyMembersWithName(accessedMemberName) - .SingleOrDefault(); - if (foundNameMatches != null) - { - return foundNameMatches; - } - } - } - catch (InvalidOperationException) { } - - var accessedMemberFullName = matchFunction.MatchNameFunction(fullName); - return accessedMemberFullName != null - ? GetPropertyMemberWithFullNameEndingWith(_type, accessedMemberFullName) - : null; - } - - private PropertyMember GetPropertyMemberWithFullNameEndingWith( - IType type, - string detailedName - ) - { - return type - .Members.OfType() - .FirstOrDefault(propertyMember => propertyMember.FullName.EndsWith(detailedName)); - } - } - - public class MatchFunction - { - public MatchFunction(Func matchNameFunction) - { - MatchNameFunction = matchNameFunction; - } - - public Func MatchNameFunction { get; } } } diff --git a/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs b/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs index d934104f5..c2ce42182 100644 --- a/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs +++ b/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs @@ -1,21 +1,17 @@ using System.Collections.Generic; -using System.Linq; using ArchUnitNET.Domain; namespace ArchUnitNET.Loader.LoadTasks { - internal class AddTypesToNamespaces : ILoadTask + /// + /// Adds each type to its namespace's type list. Must run after all types have been + /// fully populated so that namespace queries return complete results. + /// + internal static class AddTypesToNamespaces { - private readonly List _types; - - public AddTypesToNamespaces(List types) - { - _types = types; - } - - public void Execute() + internal static void Execute(IEnumerable types) { - foreach (var type in _types) + foreach (var type in types) { ((List)type.Namespace.Types).Add(type); } diff --git a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs b/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs index d526ec6e9..0b3c7d9f9 100644 --- a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs +++ b/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs @@ -1,34 +1,28 @@ -using System.Linq; +using System.Linq; using ArchUnitNET.Domain; using Mono.Cecil; namespace ArchUnitNET.Loader.LoadTasks { - internal class CollectAssemblyAttributes : ILoadTask + /// + /// Creates attribute instances from the assembly-level custom attributes of the + /// given and adds them to the domain + /// . + /// + internal static class CollectAssemblyAttributes { - private readonly Assembly _assembly; - private readonly AssemblyDefinition _assemblyDefinition; - private readonly DomainResolver _domainResolver; - - public CollectAssemblyAttributes( + internal static void Execute( Assembly assembly, AssemblyDefinition assemblyDefinition, DomainResolver domainResolver ) { - _assembly = assembly; - _assemblyDefinition = assemblyDefinition; - _domainResolver = domainResolver; - } - - public void Execute() - { - var attributeInstances = _assemblyDefinition + var attributeInstances = assemblyDefinition .CustomAttributes.Select(attr => - attr.CreateAttributeFromCustomAttribute(_domainResolver) + attr.CreateAttributeFromCustomAttribute(domainResolver) ) .ToList(); - _assembly.AttributeInstances.AddRange(attributeInstances); + assembly.AttributeInstances.AddRange(attributeInstances); } } } diff --git a/ArchUnitNET/Loader/LoadTasks/ILoadTask.cs b/ArchUnitNET/Loader/LoadTasks/ILoadTask.cs deleted file mode 100644 index 1c491b16d..000000000 --- a/ArchUnitNET/Loader/LoadTasks/ILoadTask.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ArchUnitNET.Loader.LoadTasks -{ - internal interface ILoadTask - { - void Execute(); - } -} diff --git a/ArchUnitNET/Loader/RegexUtils.cs b/ArchUnitNET/Loader/RegexUtils.cs deleted file mode 100644 index b9503a615..000000000 --- a/ArchUnitNET/Loader/RegexUtils.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Text.RegularExpressions; - -namespace ArchUnitNET.Loader -{ - public static class RegexUtils - { - private static readonly Regex GetMethodPropertyMemberRegex = new Regex(@"get_(.+)\(\)"); - private static readonly Regex SetMethodPropertyMemberRegex = new Regex(@"set_(.+)\((.+)\)"); - - public static string MatchGetPropertyName(string methodName) - { - var match = GetMethodPropertyMemberRegex.Match(methodName); - if (!match.Success) - { - return null; - } - - var accessedPropertyName = match.Groups[1].Value; - return accessedPropertyName; - } - - public static string MatchSetPropertyName(string methodName) - { - var match = SetMethodPropertyMemberRegex.Match(methodName); - if (!match.Success) - { - return null; - } - - var accessedPropertyName = match.Groups[1].Value; - return accessedPropertyName; - } - - public static bool MatchNamespaces(string goalNamespace, string currentNamespace) - { - return goalNamespace == null || currentNamespace.StartsWith(goalNamespace); - } - } -} diff --git a/ArchUnitNETTests/Domain/Extensions/TypeExtensionTests.cs b/ArchUnitNETTests/Domain/Extensions/TypeExtensionTests.cs index 021899dd7..e7386e52c 100644 --- a/ArchUnitNETTests/Domain/Extensions/TypeExtensionTests.cs +++ b/ArchUnitNETTests/Domain/Extensions/TypeExtensionTests.cs @@ -28,7 +28,7 @@ public TypeExtensionTests() .SingleOrDefault(); _exampleAttribute = Architecture.GetClassOfType(typeof(ExampleAttribute)); - _regexUtilsTests = Architecture.GetClassOfType(typeof(RegexUtilsTest)); + _regexUtilsTests = Architecture.GetClassOfType(typeof(BackingFieldExamples)); } private static readonly Architecture Architecture = diff --git a/ArchUnitNETTests/Loader/RegexUtilsTests.cs b/ArchUnitNETTests/Loader/RegexUtilsTests.cs index 05d5636e4..0bb6c3bfe 100644 --- a/ArchUnitNETTests/Loader/RegexUtilsTests.cs +++ b/ArchUnitNETTests/Loader/RegexUtilsTests.cs @@ -1,151 +1,7 @@ -using System.Linq; -using System.Text; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Extensions; -using ArchUnitNET.Loader; using ArchUnitNETTests.Domain.Dependencies.Members; -using Xunit; namespace ArchUnitNETTests.Loader { - public class RegexUtilsTest - { - private static readonly Architecture Architecture = - StaticTestArchitectures.ArchUnitNETTestArchitecture; - private static readonly string _nonMatch = "Not expected to match."; - private readonly PropertyMember _autoPropertyMember; - private readonly string _expectedGetMethodFullName; - private readonly string _expectedGetMethodName; - private readonly string _expectedSetMethodName; - private readonly string _nonMatchEmpty = string.Empty; - - public RegexUtilsTest() - { - var propertyClass = Architecture.GetClassOfType(typeof(BackingFieldExamples)); - _autoPropertyMember = propertyClass.GetPropertyMembersWithName("AutoProperty").Single(); - _expectedGetMethodName = BuildExpectedGetMethodName(_autoPropertyMember); - _expectedGetMethodFullName = BuildExpectedGetMethodFullName(_autoPropertyMember); - _expectedSetMethodName = BuildExpectedSetMethodName( - _autoPropertyMember, - _autoPropertyMember.DeclaringType - ); - } - - private static string BuildExpectedGetMethodName( - PropertyMember propertyMember, - params IType[] parameterTypes - ) - { - var builder = new StringBuilder(); - builder.Append("get_"); - builder.Append(propertyMember.Name); - builder = AddParameterTypesToMethodName(builder, parameterTypes); - return builder.ToString(); - } - - private static string BuildExpectedSetMethodName( - PropertyMember propertyMember, - params IType[] parameterTypes - ) - { - var builder = new StringBuilder(); - builder.Append("set_"); - builder.Append(propertyMember.Name); - builder = AddParameterTypesToMethodName(builder, parameterTypes); - return builder.ToString(); - } - - private static string BuildExpectedGetMethodFullName( - PropertyMember propertyMember, - params IType[] parameterTypes - ) - { - var builder = new StringBuilder(); - builder.Append(propertyMember.DeclaringType.FullName); - builder.Append(" "); - builder.Append(propertyMember.DeclaringType.Name); - builder.Append("::get_"); - builder.Append(propertyMember.Name); - builder = AddParameterTypesToMethodName(builder, parameterTypes); - return builder.ToString(); - } - - private static StringBuilder AddParameterTypesToMethodName( - StringBuilder nameBuilder, - params IType[] parameterTypeNames - ) - { - nameBuilder.Append("("); - for (var index = 0; index < parameterTypeNames.Length; ++index) - { - if (index > 0) - { - nameBuilder.Append(","); - } - - nameBuilder.Append(parameterTypeNames[index].FullName); - } - - nameBuilder.Append(")"); - return nameBuilder; - } - - [Fact] - public void GetMethodFullNameRegexRecognizesNonMatch() - { - Assert.Null(RegexUtils.MatchGetPropertyName(_nonMatchEmpty)); - Assert.Null(RegexUtils.MatchGetPropertyName(_nonMatch)); - } - - [Fact] - public void GetMethodNameRegexRecognizesNonMatch() - { - Assert.Null(RegexUtils.MatchGetPropertyName(_nonMatchEmpty)); - Assert.Null(RegexUtils.MatchGetPropertyName(_nonMatch)); - } - - [Fact] - public void GetMethodPropertyMemberFullNameRegexMatchAsExpected() - { - Assert.Equal( - _autoPropertyMember.Name, - RegexUtils.MatchGetPropertyName(_expectedGetMethodFullName) - ); - } - - [Fact] - public void GetMethodPropertyMemberRegexMatchAsExpected() - { - Assert.Equal( - _autoPropertyMember.Name, - RegexUtils.MatchGetPropertyName(_expectedGetMethodName) - ); - } - - [Fact] - public void SetMethodFullNameRegexRecognizesNonMatch() - { - Assert.Null(RegexUtils.MatchSetPropertyName(_nonMatchEmpty)); - Assert.Null(RegexUtils.MatchSetPropertyName(_nonMatch)); - } - - [Fact] - public void SetMethodNameRegexRecognizesNonMatch() - { - Assert.Null(RegexUtils.MatchSetPropertyName(_nonMatchEmpty)); - Assert.Null(RegexUtils.MatchSetPropertyName(_nonMatch)); - } - - [Fact] - public void SetMethodPropertyMemberRegexMatchAsExpected() - { - Assert.Equal( - _autoPropertyMember.Name, - RegexUtils.MatchSetPropertyName(_expectedSetMethodName) - ); - } - } - public class BackingFieldExamples { private ChildField _fieldPropertyPair; From e36a9fab5bae2236aae96e28e9f86d2720cc99cc Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:39:10 +0100 Subject: [PATCH 10/15] refactor: restructure ArchLoader with deferred LoadInstruction pattern Replace immediate module loading with a deferred instruction pattern. Introduce LoadInstruction hierarchy (FileLoadInstruction, DirectoryLoadInstruction) to accumulate load specifications. Create resolver and builder locally in Build() with try/finally for proper disposal. Make LoadModule and AddReferencedAssembliesRecursively static, removing instance field dependencies on _archBuilder and _assemblyResolver. Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchLoader.cs | 305 ++++++++++++++++++++++--------- 1 file changed, 218 insertions(+), 87 deletions(-) diff --git a/ArchUnitNET/Loader/ArchLoader.cs b/ArchUnitNET/Loader/ArchLoader.cs index 74b0da958..afafb800a 100644 --- a/ArchUnitNET/Loader/ArchLoader.cs +++ b/ArchUnitNET/Loader/ArchLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,106 +12,249 @@ namespace ArchUnitNET.Loader { public class ArchLoader { - private readonly ArchBuilder _archBuilder = new ArchBuilder(); - private DotNetCoreAssemblyResolver _assemblyResolver = new DotNetCoreAssemblyResolver(); + private readonly List _loadInstructions = new List(); - public Architecture Build() + // --------------------------------------------------------------------------- + // LoadInstruction hierarchy + // --------------------------------------------------------------------------- + + private abstract class LoadInstruction { - var architecture = _archBuilder.Build(); - _assemblyResolver.Dispose(); - _assemblyResolver = new DotNetCoreAssemblyResolver(); + public bool IncludeDependencies { get; } + public bool Recursive { get; } - return architecture; + protected LoadInstruction(bool includeDependencies, bool recursive) + { + IncludeDependencies = includeDependencies; + Recursive = recursive; + } } - public ArchLoader LoadAssemblies(params Assembly[] assemblies) + private sealed class FileLoadInstruction : LoadInstruction { - var assemblySet = new HashSet(assemblies); - assemblySet.ForEach(assembly => LoadAssembly(assembly)); - return this; + public string FileName { get; } + public string NamespaceFilter { get; } + public FilterFunc FilterFunc { get; } + + public FileLoadInstruction( + string fileName, + string namespaceFilter, + bool includeDependencies, + bool recursive, + FilterFunc filterFunc = null + ) + : base(includeDependencies, recursive) + { + FileName = fileName; + NamespaceFilter = namespaceFilter; + FilterFunc = filterFunc; + } } - public ArchLoader LoadAssembliesIncludingDependencies(params Assembly[] assemblies) + private sealed class DirectoryLoadInstruction : LoadInstruction { - return LoadAssembliesIncludingDependencies(assemblies, false); + public string Directory { get; } + public string DirectoryFilter { get; } + public SearchOption SearchOption { get; } + + public DirectoryLoadInstruction( + string directory, + string directoryFilter, + SearchOption searchOption, + bool includeDependencies, + bool recursive + ) + : base(includeDependencies, recursive) + { + Directory = directory; + DirectoryFilter = directoryFilter; + SearchOption = searchOption; + } } - public ArchLoader LoadAssembliesIncludingDependencies( - IEnumerable assemblies, - bool recursive - ) + // --------------------------------------------------------------------------- + // Build + // --------------------------------------------------------------------------- + + public Architecture Build() { - var assemblySet = new HashSet(assemblies); - assemblySet.ForEach(assembly => LoadAssemblyIncludingDependencies(assembly, recursive)); - return this; + var assemblyResolver = new DotNetCoreAssemblyResolver(); + try + { + var archBuilder = new ArchBuilder(); + foreach (var instruction in _loadInstructions) + { + if (instruction is DirectoryLoadInstruction dir) + { + ProcessInstruction(assemblyResolver, archBuilder, dir); + } + else + { + ProcessInstruction( + assemblyResolver, + archBuilder, + (FileLoadInstruction)instruction + ); + } + } + + return archBuilder.Build(); + } + finally + { + assemblyResolver.Dispose(); + } } - public ArchLoader LoadFilteredDirectory( - string directory, - string filter, - SearchOption searchOption = TopDirectoryOnly + // --------------------------------------------------------------------------- + // Instruction processing + // --------------------------------------------------------------------------- + + private static void ProcessInstruction( + DotNetCoreAssemblyResolver assemblyResolver, + ArchBuilder archBuilder, + DirectoryLoadInstruction instruction ) { - var path = Path.GetFullPath(directory); - _assemblyResolver.AssemblyPath = path; - var assemblies = Directory.GetFiles(path, filter, searchOption); - - var result = this; - return assemblies.Aggregate( - result, - (current, assembly) => current.LoadAssembly(assembly, false, false) + var path = Path.GetFullPath(instruction.Directory); + assemblyResolver.AssemblyPath = path; + var files = System.IO.Directory.GetFiles( + path, + instruction.DirectoryFilter, + instruction.SearchOption ); + foreach (var file in files) + { + LoadModule( + assemblyResolver, + archBuilder, + file, + null, + instruction.IncludeDependencies, + instruction.Recursive + ); + } } - public ArchLoader LoadFilteredDirectoryIncludingDependencies( - string directory, - string filter, - bool recursive = false, - SearchOption searchOption = TopDirectoryOnly + private static void ProcessInstruction( + DotNetCoreAssemblyResolver assemblyResolver, + ArchBuilder archBuilder, + FileLoadInstruction instruction ) { - var path = Path.GetFullPath(directory); - _assemblyResolver.AssemblyPath = path; - var assemblies = Directory.GetFiles(path, filter, searchOption); - - var result = this; - return assemblies.Aggregate( - result, - (current, assembly) => current.LoadAssembly(assembly, true, recursive) + LoadModule( + assemblyResolver, + archBuilder, + instruction.FileName, + instruction.NamespaceFilter, + instruction.IncludeDependencies, + instruction.Recursive, + instruction.FilterFunc ); } - public ArchLoader LoadNamespacesWithinAssembly(Assembly assembly, params string[] namespc) + // --------------------------------------------------------------------------- + // Public loading API + // --------------------------------------------------------------------------- + + public ArchLoader LoadAssemblies(params Assembly[] assemblies) { - var nameSpaces = new HashSet(namespc); - nameSpaces.ForEach(nameSpace => + foreach (var assembly in new HashSet(assemblies)) { - LoadModule(assembly.Location, nameSpace, false, false); - }); + _loadInstructions.Add( + new FileLoadInstruction(assembly.Location, null, false, false) + ); + } return this; } - public ArchLoader LoadAssembly(Assembly assembly) + public ArchLoader LoadAssembly(Assembly assembly) => LoadAssemblies(assembly); + + public ArchLoader LoadAssembliesIncludingDependencies(params Assembly[] assemblies) => + LoadAssembliesIncludingDependencies(assemblies, false); + + public ArchLoader LoadAssembliesIncludingDependencies( + IEnumerable assemblies, + bool recursive + ) { - return LoadAssembly(assembly.Location, false, false); + foreach (var assembly in new HashSet(assemblies)) + { + _loadInstructions.Add( + new FileLoadInstruction(assembly.Location, null, true, recursive) + ); + } + return this; } public ArchLoader LoadAssemblyIncludingDependencies( Assembly assembly, bool recursive = false + ) => LoadAssembliesIncludingDependencies(new[] { assembly }, recursive); + + public ArchLoader LoadNamespacesWithinAssembly(Assembly assembly, params string[] namespc) + { + foreach (var nameSpace in new HashSet(namespc)) + { + _loadInstructions.Add( + new FileLoadInstruction(assembly.Location, nameSpace, false, false) + ); + } + return this; + } + + /// + /// Loads assemblies from dependency tree with user-defined filtration logic + /// + /// Assemblies to start traversal from + /// Delegate to control loading and traversal logic + /// + public ArchLoader LoadAssembliesRecursively( + IEnumerable assemblies, + FilterFunc filterFunc ) { - return LoadAssembly(assembly.Location, true, recursive); + foreach (var assembly in assemblies) + { + _loadInstructions.Add( + new FileLoadInstruction(assembly.Location, null, true, true, filterFunc) + ); + } + return this; } - private ArchLoader LoadAssembly(string fileName, bool includeDependencies, bool recursive) + public ArchLoader LoadFilteredDirectory( + string directory, + string filter, + SearchOption searchOption = TopDirectoryOnly + ) { - LoadModule(fileName, null, includeDependencies, recursive); + _loadInstructions.Add( + new DirectoryLoadInstruction(directory, filter, searchOption, false, false) + ); + return this; + } + public ArchLoader LoadFilteredDirectoryIncludingDependencies( + string directory, + string filter, + bool recursive = false, + SearchOption searchOption = TopDirectoryOnly + ) + { + _loadInstructions.Add( + new DirectoryLoadInstruction(directory, filter, searchOption, true, recursive) + ); return this; } - private void LoadModule( + // --------------------------------------------------------------------------- + // Core module loading + // --------------------------------------------------------------------------- + + private static void LoadModule( + DotNetCoreAssemblyResolver assemblyResolver, + ArchBuilder archBuilder, string fileName, string nameSpace, bool includeDependencies, @@ -123,17 +266,19 @@ private void LoadModule( { var module = ModuleDefinition.ReadModule( fileName, - new ReaderParameters { AssemblyResolver = _assemblyResolver } + new ReaderParameters { AssemblyResolver = assemblyResolver } ); var processedAssemblies = new HashSet { module.Assembly.Name.FullName }; var resolvedModules = new List(); - _assemblyResolver.AddLib(module.Assembly); - _archBuilder.AddAssembly(module.Assembly, false); + assemblyResolver.AddLib(module.Assembly); + archBuilder.AddAssembly(module.Assembly, false); foreach (var assemblyReference in module.AssemblyReferences) { if (includeDependencies && recursive) { AddReferencedAssembliesRecursively( + assemblyResolver, + archBuilder, assemblyReference, processedAssemblies, resolvedModules, @@ -145,13 +290,13 @@ private void LoadModule( try { processedAssemblies.Add(assemblyReference.FullName); - _assemblyResolver.AddLib(assemblyReference); + assemblyResolver.AddLib(assemblyReference); if (includeDependencies) { var assemblyDefinition = - _assemblyResolver.Resolve(assemblyReference) + assemblyResolver.Resolve(assemblyReference) ?? throw new AssemblyResolutionException(assemblyReference); - _archBuilder.AddAssembly(assemblyDefinition, false); + archBuilder.AddAssembly(assemblyDefinition, false); resolvedModules.AddRange(assemblyDefinition.Modules); } } @@ -162,10 +307,10 @@ private void LoadModule( } } - _archBuilder.LoadTypesForModule(module, nameSpace); + archBuilder.LoadTypesForModule(module, nameSpace); foreach (var moduleDefinition in resolvedModules) { - _archBuilder.LoadTypesForModule(moduleDefinition, null); + archBuilder.LoadTypesForModule(moduleDefinition, null); } } catch (BadImageFormatException) @@ -174,7 +319,9 @@ private void LoadModule( } } - private void AddReferencedAssembliesRecursively( + private static void AddReferencedAssembliesRecursively( + DotNetCoreAssemblyResolver assemblyResolver, + ArchBuilder archBuilder, AssemblyNameReference currentAssemblyReference, ICollection processedAssemblies, List resolvedModules, @@ -189,15 +336,15 @@ FilterFunc filterFunc processedAssemblies.Add(currentAssemblyReference.FullName); try { - _assemblyResolver.AddLib(currentAssemblyReference); + assemblyResolver.AddLib(currentAssemblyReference); var assemblyDefinition = - _assemblyResolver.Resolve(currentAssemblyReference) + assemblyResolver.Resolve(currentAssemblyReference) ?? throw new AssemblyResolutionException(currentAssemblyReference); var filterResult = filterFunc?.Invoke(assemblyDefinition); if (filterResult?.LoadThisAssembly != false) { - _archBuilder.AddAssembly(assemblyDefinition, false); + archBuilder.AddAssembly(assemblyDefinition, false); resolvedModules.AddRange(assemblyDefinition.Modules); } @@ -209,6 +356,8 @@ var reference in assemblyDefinition.Modules.SelectMany(m => { if (filterResult?.TraverseDependencies != false) AddReferencedAssembliesRecursively( + assemblyResolver, + archBuilder, reference, processedAssemblies, resolvedModules, @@ -221,23 +370,5 @@ var reference in assemblyDefinition.Modules.SelectMany(m => //Failed to resolve assembly, skip it } } - - /// - /// Loads assemblies from dependency tree with user-defined filtration logic - /// - /// Assemblies to start traversal from - /// Delegate to control loading and traversal logic - /// - public ArchLoader LoadAssembliesRecursively( - IEnumerable assemblies, - FilterFunc filterFunc - ) - { - foreach (var assembly in assemblies) - { - LoadModule(assembly.Location, null, true, true, filterFunc); - } - return this; - } } } From 744c22e22a84beaa341a0d786beee51c9e9011fe Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:40:25 +0100 Subject: [PATCH 11/15] refactor: inline ObjectProviderCache into Architecture Replace separate ObjectProviderCache class with a ConcurrentDictionary field directly in Architecture. Inline the cache lookup logic into GetOrCreateObjects. Delete ObjectProviderCache.cs. Signed-off-by: Alexander Linne --- ArchUnitNET/Domain/Architecture.cs | 13 ++++++--- ArchUnitNET/Domain/ObjectProviderCache.cs | 33 ----------------------- 2 files changed, 10 insertions(+), 36 deletions(-) delete mode 100644 ArchUnitNET/Domain/ObjectProviderCache.cs diff --git a/ArchUnitNET/Domain/Architecture.cs b/ArchUnitNET/Domain/Architecture.cs index 2e4cc0ddb..8124129e2 100644 --- a/ArchUnitNET/Domain/Architecture.cs +++ b/ArchUnitNET/Domain/Architecture.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -7,7 +8,8 @@ namespace ArchUnitNET.Domain public class Architecture { private readonly IEnumerable _allAssemblies; - private readonly ObjectProviderCache _objectProviderCache; + private readonly ConcurrentDictionary _ruleEvaluationCache = + new ConcurrentDictionary(); public Architecture( IEnumerable allAssemblies, @@ -22,7 +24,6 @@ IEnumerable referencedTypes Types = types; GenericParameters = genericParameters; ReferencedTypes = referencedTypes; - _objectProviderCache = new ObjectProviderCache(this); } public IEnumerable Assemblies => @@ -50,7 +51,13 @@ Func> providingFunction ) where T : ICanBeAnalyzed { - return _objectProviderCache.GetOrCreateObjects(objectProvider, providingFunction); + unchecked + { + var key = + (objectProvider.GetHashCode() * 397) ^ objectProvider.GetType().GetHashCode(); + return (IEnumerable) + _ruleEvaluationCache.GetOrAdd(key, _ => providingFunction(this)); + } } public override bool Equals(object obj) diff --git a/ArchUnitNET/Domain/ObjectProviderCache.cs b/ArchUnitNET/Domain/ObjectProviderCache.cs deleted file mode 100644 index 15722cae9..000000000 --- a/ArchUnitNET/Domain/ObjectProviderCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using ArchUnitNET.Fluent; - -namespace ArchUnitNET.Domain -{ - public class ObjectProviderCache - { - private readonly Architecture _architecture; - private readonly ConcurrentDictionary _cache; - - public ObjectProviderCache(Architecture architecture) - { - _architecture = architecture; - _cache = new ConcurrentDictionary(); - } - - public IEnumerable GetOrCreateObjects( - IObjectProvider objectProvider, - Func> providingFunction - ) - where T : ICanBeAnalyzed - { - unchecked - { - var key = - (objectProvider.GetHashCode() * 397) ^ objectProvider.GetType().GetHashCode(); - return (IEnumerable)_cache.GetOrAdd(key, k => providingFunction(_architecture)); - } - } - } -} From 914c83e3e050b932a78777d9dc69aad9c9c99b17 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:44:31 +0100 Subject: [PATCH 12/15] feat: add options to disable architecture and rule evaluation caches Signed-off-by: Alexander Linne --- ArchUnitNET/Domain/Architecture.cs | 35 +++++++++++++++++++++- ArchUnitNET/Domain/ArchitectureCache.cs | 30 +++++++++++++++++++ ArchUnitNET/Domain/ArchitectureCacheKey.cs | 26 ++++++++++++++++ ArchUnitNET/Loader/ArchBuilder.cs | 28 ++++++++++++----- ArchUnitNET/Loader/ArchLoader.cs | 34 +++++++++++++++++++-- 5 files changed, 142 insertions(+), 11 deletions(-) diff --git a/ArchUnitNET/Domain/Architecture.cs b/ArchUnitNET/Domain/Architecture.cs index 8124129e2..bbeb130ed 100644 --- a/ArchUnitNET/Domain/Architecture.cs +++ b/ArchUnitNET/Domain/Architecture.cs @@ -5,18 +5,41 @@ namespace ArchUnitNET.Domain { + /// + /// Represents a loaded and analyzed software architecture, containing all assemblies, + /// namespaces, types, and their relationships. + /// + /// + /// An architecture is typically created via . Rule evaluation + /// results are cached by default to avoid recomputing the same provider multiple times. + /// Use to disable this caching. + /// public class Architecture { private readonly IEnumerable _allAssemblies; + private readonly bool _useRuleEvaluationCache; private readonly ConcurrentDictionary _ruleEvaluationCache = new ConcurrentDictionary(); + /// + /// Creates a new architecture from the given domain elements. + /// + /// All assemblies in the architecture, including referenced-only assemblies. + /// The namespaces containing the loaded types. + /// The types loaded from the assemblies. + /// Generic parameters found in the loaded types. + /// Types referenced but not directly loaded. + /// + /// If true (the default), rule evaluation results are cached by object provider + /// hash code and type. Pass false to disable caching. + /// public Architecture( IEnumerable allAssemblies, IEnumerable namespaces, IEnumerable types, IEnumerable genericParameters, - IEnumerable referencedTypes + IEnumerable referencedTypes, + bool useRuleEvaluationCache = true ) { _allAssemblies = allAssemblies; @@ -24,6 +47,7 @@ IEnumerable referencedTypes Types = types; GenericParameters = genericParameters; ReferencedTypes = referencedTypes; + _useRuleEvaluationCache = useRuleEvaluationCache; } public IEnumerable Assemblies => @@ -45,12 +69,21 @@ IEnumerable referencedTypes public IEnumerable MethodMembers => Members.OfType(); public IEnumerable Members => Types.SelectMany(type => type.Members); + /// + /// Returns the cached result for the given object provider, or invokes the providing + /// function and caches the result. Caching can be disabled via + /// . + /// public IEnumerable GetOrCreateObjects( IObjectProvider objectProvider, Func> providingFunction ) where T : ICanBeAnalyzed { + if (!_useRuleEvaluationCache) + { + return providingFunction(this); + } unchecked { var key = diff --git a/ArchUnitNET/Domain/ArchitectureCache.cs b/ArchUnitNET/Domain/ArchitectureCache.cs index 7e47e9a62..43c909e10 100644 --- a/ArchUnitNET/Domain/ArchitectureCache.cs +++ b/ArchUnitNET/Domain/ArchitectureCache.cs @@ -2,6 +2,19 @@ namespace ArchUnitNET.Domain { + /// + /// A singleton cache that stores instances keyed by + /// . This avoids re-loading and re-analyzing + /// assemblies when the same set of assemblies is loaded multiple times (e.g., across + /// multiple test classes that share the same architecture). + /// + /// + /// The architecture cache operates at the assembly-loading level: it caches the fully + /// constructed object. This is separate from rule evaluation + /// caching, which caches individual rule evaluation results within an architecture. + /// Use on ArchLoader to bypass this + /// cache when building an architecture. + /// public class ArchitectureCache { protected readonly ConcurrentDictionary Cache = @@ -9,8 +22,16 @@ public class ArchitectureCache protected ArchitectureCache() { } + /// + /// Gets the singleton instance of the architecture cache. + /// public static ArchitectureCache Instance { get; } = new ArchitectureCache(); + /// + /// Attempts to retrieve a cached architecture for the given key. + /// + /// The key identifying the architecture. + /// The cached architecture, or null if not found. public Architecture TryGetArchitecture(ArchitectureCacheKey architectureCacheKey) { return Cache.TryGetValue(architectureCacheKey, out var matchArchitecture) @@ -18,11 +39,20 @@ public Architecture TryGetArchitecture(ArchitectureCacheKey architectureCacheKey : null; } + /// + /// Adds an architecture to the cache. If the key already exists, the existing entry is kept. + /// + /// The key identifying the architecture. + /// The architecture to cache. + /// true if the architecture was added; false if the key already existed. public bool Add(ArchitectureCacheKey architectureCacheKey, Architecture architecture) { return Cache.TryAdd(architectureCacheKey, architecture); } + /// + /// Removes all entries from the cache. + /// public void Clear() => Cache.Clear(); } } diff --git a/ArchUnitNET/Domain/ArchitectureCacheKey.cs b/ArchUnitNET/Domain/ArchitectureCacheKey.cs index 3c075420a..510317033 100644 --- a/ArchUnitNET/Domain/ArchitectureCacheKey.cs +++ b/ArchUnitNET/Domain/ArchitectureCacheKey.cs @@ -5,22 +5,47 @@ namespace ArchUnitNET.Domain { + /// + /// Identifies a cached by the set of loaded modules, + /// their namespace filters, and whether rule evaluation caching is disabled. + /// Two keys are equal when they represent the same combination of modules, filters, + /// and caching flag, regardless of insertion order. + /// public class ArchitectureCacheKey : IEquatable { private readonly SortedSet<(string moduleName, string filter)> _architectureCacheKey = new SortedSet<(string moduleName, string filter)>(new ArchitectureCacheKeyComparer()); + private bool _ruleEvaluationCacheDisabled; + public bool Equals(ArchitectureCacheKey other) { return other != null + && _ruleEvaluationCacheDisabled == other._ruleEvaluationCacheDisabled && _architectureCacheKey.SequenceEqual(other._architectureCacheKey); } + /// + /// Adds a module and optional namespace filter to this key. + /// + /// The name of the loaded module. + /// + /// The namespace filter applied when loading, or null if no filter was used. + /// public void Add(string moduleName, string filter) { _architectureCacheKey.Add((moduleName, filter)); } + /// + /// Marks this key as representing an architecture with rule evaluation caching disabled. + /// Architectures with caching disabled are stored separately in the cache. + /// + public void SetRuleEvaluationCacheDisabled() + { + _ruleEvaluationCacheDisabled = true; + } + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) @@ -45,6 +70,7 @@ public override int GetHashCode() { hashCode = (hashCode * 131) ^ tuple.GetHashCode(); }); + hashCode = (hashCode * 131) ^ _ruleEvaluationCacheDisabled.GetHashCode(); return hashCode; } } diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index 793336837..28624be3e 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -13,7 +13,7 @@ namespace ArchUnitNET.Loader /// Internal builder that constructs an from loaded modules. /// Coordinates type discovery (via ), type processing /// (via the LoadTasks static classes), and architecture assembly. Manages the - /// lookup. + /// lookup and supports disabling rule-evaluation caching. /// internal class ArchBuilder { @@ -146,14 +146,22 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) /// /// Builds the from all loaded modules. Returns a cached - /// instance when available. + /// instance when available (unless is set). /// - public Architecture Build() + public Architecture Build(bool skipRuleEvaluationCache, bool skipArchitectureCache) { - var architecture = _architectureCache.TryGetArchitecture(_architectureCacheKey); - if (architecture != null) + if (skipRuleEvaluationCache) { - return architecture; + _architectureCacheKey.SetRuleEvaluationCacheDisabled(); + } + + if (!skipArchitectureCache) + { + var architecture = _architectureCache.TryGetArchitecture(_architectureCacheKey); + if (architecture != null) + { + return architecture; + } } ProcessTypes(); @@ -176,10 +184,14 @@ public Architecture Build() namespaces, types, genericParameters, - referencedTypes + referencedTypes, + !skipRuleEvaluationCache ); - _architectureCache.Add(_architectureCacheKey, newArchitecture); + if (!skipArchitectureCache) + { + _architectureCache.Add(_architectureCacheKey, newArchitecture); + } return newArchitecture; } diff --git a/ArchUnitNET/Loader/ArchLoader.cs b/ArchUnitNET/Loader/ArchLoader.cs index afafb800a..be28272df 100644 --- a/ArchUnitNET/Loader/ArchLoader.cs +++ b/ArchUnitNET/Loader/ArchLoader.cs @@ -12,10 +12,12 @@ namespace ArchUnitNET.Loader { public class ArchLoader { + private bool _skipRuleEvaluationCache; + private bool _skipArchitectureCache; private readonly List _loadInstructions = new List(); // --------------------------------------------------------------------------- - // LoadInstruction hierarchy + // LoadInstruction hierarchy — replaces the old tagged-union struct // --------------------------------------------------------------------------- private abstract class LoadInstruction @@ -72,6 +74,34 @@ bool recursive } } + // --------------------------------------------------------------------------- + // Cache configuration + // --------------------------------------------------------------------------- + + /// + /// Configures this loader to disable rule evaluation caching in the built architecture. + /// Each call to will invoke the providing + /// function directly instead of returning a cached result. + /// + /// This loader instance for method chaining. + public ArchLoader WithoutRuleEvaluationCache() + { + _skipRuleEvaluationCache = true; + return this; + } + + /// + /// Configures this loader to skip the global when + /// building the architecture. Each call to will create a fresh + /// architecture instance instead of returning a cached one. + /// + /// This loader instance for method chaining. + public ArchLoader WithoutArchitectureCache() + { + _skipArchitectureCache = true; + return this; + } + // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- @@ -98,7 +128,7 @@ public Architecture Build() } } - return archBuilder.Build(); + return archBuilder.Build(_skipRuleEvaluationCache, _skipArchitectureCache); } finally { From cccf20140e46e60461e01eb2836242fde9be4de3 Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 14:47:20 +0100 Subject: [PATCH 13/15] test: add cache configuration tests, update fixtures, and document caching Signed-off-by: Alexander Linne --- .../Domain/ArchitectureCacheKeyTests.cs | 32 ++++ ArchUnitNETTests/Loader/ArchLoaderTests.cs | 164 ++++++++++++++++++ ArchUnitNETTests/StaticTestArchitectures.cs | 16 ++ documentation/docs/guide.md | 24 +++ 4 files changed, 236 insertions(+) diff --git a/ArchUnitNETTests/Domain/ArchitectureCacheKeyTests.cs b/ArchUnitNETTests/Domain/ArchitectureCacheKeyTests.cs index cfae7ae60..60c99c51b 100644 --- a/ArchUnitNETTests/Domain/ArchitectureCacheKeyTests.cs +++ b/ArchUnitNETTests/Domain/ArchitectureCacheKeyTests.cs @@ -120,5 +120,37 @@ public void SameObjectReferenceIsSameArchitectureCacheKet() Assert.True(_architectureCacheKey.Equals(referenceDuplicate)); } + + [Fact] + public void KeysWithDifferentRuleEvaluationCacheSettingsAreNotEqual() + { + _architectureCacheKey.Add(_baseClassModuleName, null); + // cache enabled (default) + + _duplicateArchitectureCacheKey.Add(_baseClassModuleName, null); + _duplicateArchitectureCacheKey.SetRuleEvaluationCacheDisabled(); + + Assert.NotEqual(_architectureCacheKey, _duplicateArchitectureCacheKey); + Assert.NotEqual( + _architectureCacheKey.GetHashCode(), + _duplicateArchitectureCacheKey.GetHashCode() + ); + } + + [Fact] + public void KeysBothWithCacheDisabledAreEqual() + { + _architectureCacheKey.Add(_baseClassModuleName, null); + _architectureCacheKey.SetRuleEvaluationCacheDisabled(); + + _duplicateArchitectureCacheKey.Add(_baseClassModuleName, null); + _duplicateArchitectureCacheKey.SetRuleEvaluationCacheDisabled(); + + Assert.Equal(_architectureCacheKey, _duplicateArchitectureCacheKey); + Assert.Equal( + _architectureCacheKey.GetHashCode(), + _duplicateArchitectureCacheKey.GetHashCode() + ); + } } } diff --git a/ArchUnitNETTests/Loader/ArchLoaderTests.cs b/ArchUnitNETTests/Loader/ArchLoaderTests.cs index 5d9408025..e7e586e6f 100644 --- a/ArchUnitNETTests/Loader/ArchLoaderTests.cs +++ b/ArchUnitNETTests/Loader/ArchLoaderTests.cs @@ -2,10 +2,12 @@ extern alias OtherLoaderTestAssemblyAlias; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using ArchUnitNET.Domain; using ArchUnitNET.Domain.Extensions; +using ArchUnitNET.Fluent; using ArchUnitNET.Loader; using ArchUnitNET.xUnit; using ArchUnitNETTests.Domain.Dependencies.Members; @@ -20,6 +22,168 @@ namespace ArchUnitNETTests.Loader { public class ArchLoaderTests { + [Fact] + public void WithoutRuleEvaluationCacheDisablesCaching() + { + // Build an architecture with caching disabled and verify that + // GetOrCreateObjects always invokes the providing function. + var architecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() + .LoadAssembly(typeof(BaseClass).Assembly) + .Build(); + + var callCount = 0; + var provider = new TestObjectProvider("same-description"); + + architecture.GetOrCreateObjects( + provider, + _ => + { + callCount++; + return Enumerable.Empty(); + } + ); + architecture.GetOrCreateObjects( + provider, + _ => + { + callCount++; + return Enumerable.Empty(); + } + ); + + // Caching disabled — function should be invoked every time + Assert.Equal(2, callCount); + } + + [Fact] + public void DefaultCacheCachesRuleEvaluationResults() + { + // Build an architecture with the default cache and verify that + // GetOrCreateObjects caches the result for the same provider. + var architecture = new ArchLoader() + .WithoutArchitectureCache() + .LoadAssembly(typeof(BaseClass).Assembly) + .Build(); + + var callCount = 0; + var provider = new TestObjectProvider("default-cache-test"); + + architecture.GetOrCreateObjects( + provider, + _ => + { + callCount++; + return Enumerable.Empty(); + } + ); + architecture.GetOrCreateObjects( + provider, + _ => + { + callCount++; + return Enumerable.Empty(); + } + ); + + // Default caching — function should be invoked only once + Assert.Equal(1, callCount); + } + + [Fact] + public void WithoutArchitectureCacheCreatesFreshArchitecture() + { + // Two architectures built with WithoutArchitectureCache for the same assembly + // should be distinct object instances. + var arch1 = new ArchLoader() + .WithoutArchitectureCache() + .LoadAssembly(typeof(BaseClass).Assembly) + .Build(); + + var arch2 = new ArchLoader() + .WithoutArchitectureCache() + .LoadAssembly(typeof(BaseClass).Assembly) + .Build(); + + Assert.NotSame(arch1, arch2); + } + + [Fact] + public void WithoutRuleEvaluationCacheAndWithoutArchitectureCacheCombined() + { + // Build two architectures with caching disabled + WithoutArchitectureCache + // from the same assembly. They should be distinct instances, each with no caching. + var arch1 = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() + .LoadAssembly(typeof(BaseClass).Assembly) + .Build(); + + var arch2 = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() + .LoadAssembly(typeof(BaseClass).Assembly) + .Build(); + + Assert.NotSame(arch1, arch2); + + // Verify caching is disabled + var callCount = 0; + var provider = new TestObjectProvider("combined-test"); + + arch1.GetOrCreateObjects( + provider, + _ => + { + callCount++; + return Enumerable.Empty(); + } + ); + arch1.GetOrCreateObjects( + provider, + _ => + { + callCount++; + return Enumerable.Empty(); + } + ); + + Assert.Equal(2, callCount); + } + + /// + /// Minimal IObjectProvider implementation for ArchLoader integration tests. + /// + private class TestObjectProvider : IObjectProvider + { + public TestObjectProvider(string description) + { + Description = description; + } + + public string Description { get; } + + public IEnumerable GetObjects(Architecture architecture) + { + return Enumerable.Empty(); + } + + public string FormatDescription( + string emptyDescription, + string singleDescription, + string multipleDescription + ) + { + return Description; + } + + public override int GetHashCode() + { + return Description != null ? Description.GetHashCode() : 0; + } + } + [Fact] public void LoadAssemblies() { diff --git a/ArchUnitNETTests/StaticTestArchitectures.cs b/ArchUnitNETTests/StaticTestArchitectures.cs index 28ea6143d..ca69fa9c7 100644 --- a/ArchUnitNETTests/StaticTestArchitectures.cs +++ b/ArchUnitNETTests/StaticTestArchitectures.cs @@ -20,18 +20,32 @@ public static class StaticTestArchitectures .Build(); public static readonly Architecture AttributeArchitecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() .LoadAssemblies(typeof(AttributeNamespace.ClassWithoutAttributes).Assembly) .Build(); public static readonly Architecture DependencyArchitecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() .LoadAssemblies(typeof(TypeDependencyNamespace.BaseClass).Assembly) .Build(); + public static readonly Architecture MethodDependencyArchitecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() + .LoadAssemblies(typeof(MethodDependencyNamespace.MethodDependencyClass).Assembly) + .Build(); + public static readonly Architecture InterfaceArchitecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() .LoadAssemblies(typeof(InterfaceAssembly.IBaseInterface).Assembly) .Build(); public static readonly Architecture LoaderTestArchitecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() .LoadAssemblies( typeof(LoaderTestAssembly.LoaderTestAssembly).Assembly, typeof(OtherLoaderTestAssembly.OtherLoaderTestAssembly).Assembly @@ -39,6 +53,8 @@ public static class StaticTestArchitectures .Build(); public static readonly Architecture VisibilityArchitecture = new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() .LoadAssemblies(typeof(VisibilityNamespace.PublicClass).Assembly) .Build(); diff --git a/documentation/docs/guide.md b/documentation/docs/guide.md index 0ed4d8dd4..b9c4cc63a 100644 --- a/documentation/docs/guide.md +++ b/documentation/docs/guide.md @@ -40,6 +40,30 @@ private static readonly Architecture Architecture = System.Reflection.Assembly.Load("ForbiddenClassAssemblyName") ).Build(); ``` +#### 2.2.1. Caching + +ArchUnitNET uses two levels of caching to improve performance: + +**Architecture Cache** — When you call `Build()`, the resulting `Architecture` is stored in a global singleton cache keyed by the loaded assemblies. If you load the same set of assemblies again (e.g., in a different test class), the cached architecture is returned instead of re-analyzing everything. + +**Rule Evaluation Cache** — When architecture rules are evaluated, the filtered collections produced by object providers (e.g., `Types().That().ResideInNamespace(...)`) are cached within the architecture. If the same provider is used in multiple rules, the cached result is reused. + +Both caches are enabled by default. You can configure them via `ArchLoader`: + +```cs +// Disable rule evaluation caching (every rule evaluates its providers independently) +private static readonly Architecture Architecture = + new ArchLoader() + .WithoutRuleEvaluationCache() + .WithoutArchitectureCache() + .LoadAssemblies(System.Reflection.Assembly.Load("ExampleClassAssemblyName")) + .Build(); +``` + +`WithoutRuleEvaluationCache()` disables the rule evaluation cache so that each call to `GetOrCreateObjects` always executes the provider's filtering logic. This is useful in test scenarios where you need to ensure every provider independently exercises its code path. + +`WithoutArchitectureCache()` bypasses the global architecture cache, so each `Build()` call creates a fresh architecture instance. This is typically used together with `WithoutRuleEvaluationCache()` to avoid caching an architecture with non-standard caching settings in the global cache. + #### 2.3. Declare Layers Declare variables you'll use throughout your tests up here ```cs From 111a21661f529960696f6572ead5f28568a84e9b Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 20 Mar 2026 15:04:53 +0100 Subject: [PATCH 14/15] refactor: consolidate load task classes into single TypeProcessor class Signed-off-by: Alexander Linne --- ArchUnitNET/Loader/ArchBuilder.cs | 25 +- .../AddAttributesAndAttributeDependencies.cs | 277 ---- .../LoadTasks/AddBackwardsDependencies.cs | 29 - .../LoadTasks/AddBaseClassDependency.cs | 43 - .../Loader/LoadTasks/AddClassDependencies.cs | 55 - .../AddFieldAndPropertyDependencies.cs | 36 - .../AddGenericArgumentDependencies.cs | 108 -- .../AddGenericParameterDependencies.cs | 46 - ArchUnitNET/Loader/LoadTasks/AddMembers.cs | 160 --- .../Loader/LoadTasks/AddMethodDependencies.cs | 409 ------ .../Loader/LoadTasks/AddTypesToNamespaces.cs | 20 - .../LoadTasks/CollectAssemblyAttributes.cs | 28 - ArchUnitNET/Loader/TypeProcessor.cs | 1138 +++++++++++++++++ 13 files changed, 1150 insertions(+), 1224 deletions(-) delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddMembers.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs delete mode 100644 ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs create mode 100644 ArchUnitNET/Loader/TypeProcessor.cs diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index 28624be3e..9df3c9df8 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -2,7 +2,6 @@ using System.Linq; using ArchUnitNET.Domain; using ArchUnitNET.Domain.Extensions; -using ArchUnitNET.Loader.LoadTasks; using JetBrains.Annotations; using Mono.Cecil; using GenericParameter = ArchUnitNET.Domain.GenericParameter; @@ -12,7 +11,7 @@ namespace ArchUnitNET.Loader /// /// Internal builder that constructs an from loaded modules. /// Coordinates type discovery (via ), type processing - /// (via the LoadTasks static classes), and architecture assembly. Manages the + /// (via ), and architecture assembly. Manages the /// lookup and supports disabling rule-evaluation caching. /// internal class ArchBuilder @@ -209,7 +208,7 @@ private void ProcessTypes() // Phase 1: Base class dependency (non-interface types only) foreach (var entry in typesToProcess.Where(entry => !entry.Definition.IsInterface)) { - AddBaseClassDependency.Execute( + TypeProcessor.AddBaseClassDependency( entry.TypeInstance.Type, entry.Definition, _domainResolver @@ -225,7 +224,7 @@ MemberData MemberData )>(_typesToProcess.Count); typesWithMemberData.AddRange( from entry in typesToProcess - let memberData = AddMembers.Execute( + let memberData = TypeProcessor.AddMembers( entry.TypeInstance, entry.Definition, _domainResolver @@ -236,13 +235,13 @@ from entry in typesToProcess // Phase 3: Generic parameter dependencies foreach (var entry in typesToProcess) { - AddGenericParameterDependencies.Execute(entry.TypeInstance.Type); + TypeProcessor.AddGenericParameterDependencies(entry.TypeInstance.Type); } // Phase 4: Attributes and attribute dependencies foreach (var entry in typesWithMemberData) { - AddAttributesAndAttributeDependencies.Execute( + TypeProcessor.AddAttributesAndAttributeDependencies( entry.TypeInstance.Type, entry.TypeDef, _domainResolver, @@ -256,7 +255,7 @@ from entry in typesToProcess var assemblyData = _assemblyData.Values.ToList(); foreach (var entry in assemblyData) { - CollectAssemblyAttributes.Execute( + TypeProcessor.CollectAssemblyAttributes( entry.Assembly, entry.Definition, _domainResolver @@ -266,13 +265,13 @@ from entry in typesToProcess // Phase 6: Field and property type dependencies foreach (var entry in typesToProcess) { - AddFieldAndPropertyDependencies.Execute(entry.TypeInstance.Type); + TypeProcessor.AddFieldAndPropertyDependencies(entry.TypeInstance.Type); } // Phase 7: Method signature and body dependencies foreach (var entry in typesWithMemberData) { - AddMethodDependencies.Execute( + TypeProcessor.AddMethodDependencies( entry.TypeInstance.Type, _domainResolver, entry.MemberData.MethodPairs, @@ -283,13 +282,13 @@ from entry in typesToProcess // Phase 8: Generic argument dependencies foreach (var entry in typesToProcess) { - AddGenericArgumentDependencies.Execute(entry.TypeInstance.Type); + TypeProcessor.AddGenericArgumentDependencies(entry.TypeInstance.Type); } // Phase 9: Interface and member-to-type dependencies foreach (var entry in typesToProcess) { - AddClassDependencies.Execute( + TypeProcessor.AddClassDependencies( entry.TypeInstance.Type, entry.Definition, _domainResolver @@ -299,11 +298,11 @@ from entry in typesToProcess // Phase 10: Backwards dependencies foreach (var entry in typesToProcess) { - AddBackwardsDependencies.Execute(entry.TypeInstance.Type); + TypeProcessor.AddBackwardsDependencies(entry.TypeInstance.Type); } // Phase 11: Register types with their namespaces - AddTypesToNamespaces.Execute( + TypeProcessor.AddTypesToNamespaces( _typesToProcess.Values.Select(entry => entry.TypeInstance.Type) ); } diff --git a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs deleted file mode 100644 index 60ef7e904..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; -using JetBrains.Annotations; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Creates attribute instances from Cecil custom attributes on the type, its generic - /// parameters, and its members, then adds corresponding attribute-type dependencies. - /// - internal static class AddAttributesAndAttributeDependencies - { - internal static void Execute( - IType type, - TypeDefinition typeDefinition, - DomainResolver domainResolver, - IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs - ) - { - typeDefinition.CustomAttributes.ForEach(attr => - AddAttributeArgumentReferenceDependencies(type, attr, domainResolver) - ); - var typeAttributeInstances = CreateAttributesFromCustomAttributes( - typeDefinition.CustomAttributes, - domainResolver - ) - .ToList(); - type.AttributeInstances.AddRange(typeAttributeInstances); - var typeAttributeDependencies = typeAttributeInstances.Select( - attributeInstance => new AttributeTypeDependency(type, attributeInstance) - ); - type.Dependencies.AddRange(typeAttributeDependencies); - - SetUpAttributesForTypeGenericParameters(type, typeDefinition, domainResolver); - CollectAttributesForMembers(type, typeDefinition, domainResolver, methodPairs); - } - - private static void SetUpAttributesForTypeGenericParameters( - IType type, - TypeDefinition typeDefinition, - DomainResolver domainResolver - ) - { - foreach (var genericParameter in typeDefinition.GenericParameters) - { - var param = type.GenericParameters.First(parameter => - parameter.Name == genericParameter.Name - ); - var attributeInstances = CreateAttributesFromCustomAttributes( - genericParameter.CustomAttributes, - domainResolver - ) - .ToList(); - type.AttributeInstances.AddRange(attributeInstances); - param.AttributeInstances.AddRange(attributeInstances); - var genericParameterAttributeDependencies = attributeInstances.Select( - attributeInstance => new AttributeTypeDependency(type, attributeInstance) - ); - type.Dependencies.AddRange(genericParameterAttributeDependencies); - } - } - - private static void CollectAttributesForMembers( - IType type, - TypeDefinition typeDefinition, - DomainResolver domainResolver, - IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs - ) - { - typeDefinition - .Fields.Where(x => !x.IsBackingField() && !x.IsCompilerGenerated()) - .ForEach(fieldDef => SetUpAttributesForField(type, fieldDef, domainResolver)); - - typeDefinition - .Properties.Where(x => !x.IsCompilerGenerated()) - .ForEach(propDef => SetUpAttributesForProperty(type, propDef, domainResolver)); - - // Build a lookup from method full-name -> MethodMember to avoid O(n) scans - var methodMemberByFullName = new Dictionary(methodPairs.Count); - foreach (var pair in methodPairs) - { - var key = pair.Definition.BuildFullName(); - if (!methodMemberByFullName.ContainsKey(key)) - { - methodMemberByFullName[key] = pair.Member; - } - } - - typeDefinition - .Methods.Where(x => !x.IsCompilerGenerated()) - .ForEach(methodDef => - { - MethodMember methodMember; - if ( - !methodMemberByFullName.TryGetValue( - methodDef.BuildFullName(), - out methodMember - ) - ) - { - return; - } - - SetUpAttributesForMethod(methodDef, methodMember, type, domainResolver); - }); - } - - private static void SetUpAttributesForField( - IType type, - FieldDefinition fieldDefinition, - DomainResolver domainResolver - ) - { - var fieldMember = type.GetFieldMembers() - .WhereFullNameIs(fieldDefinition.FullName) - .RequiredNotNull(); - CollectMemberAttributesAndDependencies( - type, - fieldMember, - fieldDefinition.CustomAttributes.ToList(), - fieldMember.MemberDependencies, - domainResolver - ); - } - - private static void SetUpAttributesForProperty( - IType type, - PropertyDefinition propertyDefinition, - DomainResolver domainResolver - ) - { - var propertyMember = type.GetPropertyMembers() - .WhereFullNameIs(propertyDefinition.FullName) - .RequiredNotNull(); - CollectMemberAttributesAndDependencies( - type, - propertyMember, - propertyDefinition.CustomAttributes.ToList(), - propertyMember.AttributeDependencies, - domainResolver - ); - } - - private static void SetUpAttributesForMethod( - MethodDefinition methodDefinition, - MethodMember methodMember, - IType type, - DomainResolver domainResolver - ) - { - var memberCustomAttributes = methodDefinition.GetAllMethodCustomAttributes().ToList(); - SetUpAttributesForMethodGenericParameters( - methodDefinition, - methodMember, - domainResolver - ); - CollectMemberAttributesAndDependencies( - type, - methodMember, - memberCustomAttributes, - methodMember.MemberDependencies, - domainResolver - ); - } - - private static void SetUpAttributesForMethodGenericParameters( - MethodDefinition methodDefinition, - MethodMember methodMember, - DomainResolver domainResolver - ) - { - foreach (var genericParameter in methodDefinition.GenericParameters) - { - var param = methodMember.GenericParameters.First(parameter => - parameter.Name == genericParameter.Name - ); - var customAttributes = genericParameter.CustomAttributes; - customAttributes.ForEach(attr => - AddAttributeArgumentReferenceDependencies( - methodMember.DeclaringType, - attr, - domainResolver - ) - ); - var attributeInstances = CreateAttributesFromCustomAttributes( - customAttributes, - domainResolver - ) - .ToList(); - methodMember.AttributeInstances.AddRange(attributeInstances); - param.AttributeInstances.AddRange(attributeInstances); - var genericParameterAttributeDependencies = attributeInstances.Select( - attributeInstance => new AttributeMemberDependency( - methodMember, - attributeInstance - ) - ); - methodMember.MemberDependencies.AddRange(genericParameterAttributeDependencies); - } - } - - private static void CollectMemberAttributesAndDependencies( - IType type, - IMember member, - List memberCustomAttributes, - List attributeDependencies, - DomainResolver domainResolver - ) - { - memberCustomAttributes.ForEach(attr => - AddAttributeArgumentReferenceDependencies(type, attr, domainResolver) - ); - var memberAttributeInstances = CreateAttributesFromCustomAttributes( - memberCustomAttributes, - domainResolver - ) - .ToList(); - member.AttributeInstances.AddRange(memberAttributeInstances); - var methodAttributeDependencies = memberAttributeInstances.Select( - attributeInstance => new AttributeMemberDependency(member, attributeInstance) - ); - attributeDependencies.AddRange(methodAttributeDependencies); - } - - [NotNull] - private static IEnumerable CreateAttributesFromCustomAttributes( - IEnumerable customAttributes, - DomainResolver domainResolver - ) - { - return customAttributes - .Where(customAttribute => - customAttribute.AttributeType.FullName - != "Microsoft.CodeAnalysis.EmbeddedAttribute" - && customAttribute.AttributeType.FullName - != "System.Runtime.CompilerServices.NullableAttribute" - && customAttribute.AttributeType.FullName - != "System.Runtime.CompilerServices.NullableContextAttribute" - ) - .Select(attr => attr.CreateAttributeFromCustomAttribute(domainResolver)); - } - - private static void AddAttributeArgumentReferenceDependencies( - IType type, - ICustomAttribute customAttribute, - DomainResolver domainResolver - ) - { - if (!customAttribute.HasConstructorArguments) - { - return; - } - - var attributeConstructorArgs = customAttribute.ConstructorArguments; - attributeConstructorArgs - .Where(attributeArgument => - attributeArgument.Value is TypeReference typeReference - && !typeReference.IsCompilerGenerated() - ) - .Select(attributeArgument => - (typeReference: attributeArgument.Value as TypeReference, attributeArgument) - ) - .ForEach(tuple => - { - var argumentType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - tuple.typeReference - ); - var dependency = new TypeReferenceDependency(type, argumentType); - type.Dependencies.Add(dependency); - }); - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs deleted file mode 100644 index 409b45356..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Registers each of the type's dependencies as a backwards dependency on the - /// target type, and each member-member dependency as a backwards dependency on - /// the target member. - /// - internal static class AddBackwardsDependencies - { - internal static void Execute(IType type) - { - type.Dependencies.ForEach(dependency => - dependency.Target.BackwardsDependencies.Add(dependency) - ); - - var memberMemberDependencies = type - .Members.SelectMany(member => member.MemberDependencies) - .OfType(); - memberMemberDependencies.ForEach(memberDependency => - memberDependency.TargetMember.MemberBackwardsDependencies.Add(memberDependency) - ); - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs b/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs deleted file mode 100644 index 2f4e11b81..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs +++ /dev/null @@ -1,43 +0,0 @@ -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// If the type has a base class, creates an - /// and adds it to the type's dependency list. Skipped for interfaces and types - /// whose base is not a . - /// - internal static class AddBaseClassDependency - { - internal static void Execute( - IType type, - TypeDefinition typeDefinition, - DomainResolver domainResolver - ) - { - var baseTypeRef = typeDefinition.BaseType; - if (baseTypeRef == null) - { - return; - } - - var baseType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(baseTypeRef); - if (!(baseType.Type is Class baseClass)) - { - return; - } - - var dependency = new InheritsBaseClassDependency( - type, - new TypeInstance( - baseClass, - baseType.GenericArguments, - baseType.ArrayDimensions - ) - ); - type.Dependencies.Add(dependency); - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs deleted file mode 100644 index 4b0b5af96..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Creates for each interface the type - /// implements (including inherited interfaces), then rolls up all member-level - /// dependencies to the type's dependency list. - /// - internal static class AddClassDependencies - { - internal static void Execute( - IType type, - TypeDefinition typeDefinition, - DomainResolver domainResolver - ) - { - // Interface dependencies - GetInterfacesImplementedByClass(typeDefinition) - .ForEach(target => - { - var targetType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - target - ); - type.Dependencies.Add(new ImplementsInterfaceDependency(type, targetType)); - }); - - // Member dependencies rolled up to type level - type.Members.ForEach(member => - { - type.Dependencies.AddRange(member.Dependencies); - }); - } - - private static IEnumerable GetInterfacesImplementedByClass( - TypeDefinition typeDefinition - ) - { - var baseType = typeDefinition.BaseType?.Resolve(); - var baseInterfaces = - baseType != null - ? GetInterfacesImplementedByClass(baseType) - : new List(); - - return typeDefinition - .Interfaces.Select(implementation => implementation.InterfaceType) - .Concat(baseInterfaces); - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs deleted file mode 100644 index bec50b4f8..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs +++ /dev/null @@ -1,36 +0,0 @@ -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Creates and - /// instances for each field and property member of the type. - /// - internal static class AddFieldAndPropertyDependencies - { - internal static void Execute(IType type) - { - type.GetFieldMembers() - .ForEach(field => - { - var dependency = new FieldTypeDependency(field); - if (!field.MemberDependencies.Contains(dependency)) - { - field.MemberDependencies.Add(dependency); - } - }); - - type.GetPropertyMembers() - .ForEach(property => - { - var dependency = new PropertyTypeDependency(property); - if (!property.MemberDependencies.Contains(dependency)) - { - property.MemberDependencies.Add(dependency); - } - }); - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs deleted file mode 100644 index 1a5ac5b0b..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Propagates generic-parameter dependencies to the owning type and member, - /// then recursively discovers nested generic-argument dependencies in all - /// existing type and member dependencies. - /// - internal static class AddGenericArgumentDependencies - { - internal static void Execute(IType type) - { - // Type-level generic argument dependencies - foreach (var parameter in type.GenericParameters) - { - type.Dependencies.AddRange(parameter.Dependencies); - } - - // Member-level generic argument dependencies - foreach (var member in type.Members) - { - foreach (var parameter in member.GenericParameters) - { - member.MemberDependencies.AddRange( - parameter.Dependencies.Cast() - ); - } - } - - // Recursive generic arguments in type dependencies - var typeDependencies = new List(); - foreach (var dependency in type.Dependencies) - { - FindGenericArgumentsInTypeDependenciesRecursive( - type, - dependency.TargetGenericArguments, - typeDependencies - ); - } - - type.Dependencies.AddRange(typeDependencies); - - // Recursive generic arguments in member dependencies - foreach (var member in type.Members) - { - var memberDependencies = new List(); - foreach (var dependency in member.Dependencies) - { - FindGenericArgumentsInMemberDependenciesRecursive( - member, - dependency.TargetGenericArguments, - memberDependencies - ); - } - - member.MemberDependencies.AddRange(memberDependencies); - } - } - - private static void FindGenericArgumentsInTypeDependenciesRecursive( - IType type, - IEnumerable targetGenericArguments, - ICollection createdDependencies - ) - { - foreach ( - var genericArgument in targetGenericArguments.Where(argument => - !argument.Type.IsGenericParameter - ) - ) - { - createdDependencies.Add(new GenericArgumentTypeDependency(type, genericArgument)); - FindGenericArgumentsInTypeDependenciesRecursive( - type, - genericArgument.GenericArguments, - createdDependencies - ); - } - } - - private static void FindGenericArgumentsInMemberDependenciesRecursive( - IMember member, - IEnumerable targetGenericArguments, - ICollection createdDependencies - ) - { - foreach ( - var genericArgument in targetGenericArguments.Where(argument => - !argument.Type.IsGenericParameter - ) - ) - { - createdDependencies.Add( - new GenericArgumentMemberDependency(member, genericArgument) - ); - FindGenericArgumentsInMemberDependenciesRecursive( - member, - genericArgument.GenericArguments, - createdDependencies - ); - } - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs deleted file mode 100644 index 3cedef396..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs +++ /dev/null @@ -1,46 +0,0 @@ -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Assigns declarers to generic parameters and creates type-constraint dependencies - /// for both type-level and member-level generic parameters. - /// - internal static class AddGenericParameterDependencies - { - internal static void Execute(IType type) - { - // Type-level generic parameters - foreach (var genericParameter in type.GenericParameters) - { - genericParameter.AssignDeclarer(type); - foreach (var typeInstanceConstraint in genericParameter.TypeInstanceConstraints) - { - var dependency = new TypeGenericParameterTypeConstraintDependency( - genericParameter, - typeInstanceConstraint - ); - genericParameter.Dependencies.Add(dependency); - } - } - - // Member-level generic parameters - foreach (var member in type.Members) - { - foreach (var genericParameter in member.GenericParameters) - { - genericParameter.AssignDeclarer(member); - foreach (var typeInstanceConstraint in genericParameter.TypeInstanceConstraints) - { - var dependency = new MemberGenericParameterTypeConstraintDependency( - genericParameter, - typeInstanceConstraint - ); - genericParameter.Dependencies.Add(dependency); - } - } - } - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs b/ArchUnitNET/Loader/LoadTasks/AddMembers.cs deleted file mode 100644 index b65160913..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Extensions; -using JetBrains.Annotations; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Creates field, property, and method members from the Cecil - /// and adds them to the domain type. - /// Returns a containing method pairs and - /// a mapping from getter/setter s to - /// their s. - /// - internal static class AddMembers - { - internal static MemberData Execute( - ITypeInstance typeInstance, - TypeDefinition typeDefinition, - DomainResolver domainResolver - ) - { - var methodPairs = new List<(MethodMember Member, MethodDefinition Definition)>(); - var propertyByAccessor = new Dictionary(); - var members = CreateMembers( - typeInstance, - typeDefinition, - domainResolver, - methodPairs, - propertyByAccessor - ); - typeInstance.Type.Members.AddRange(members); - return new MemberData(methodPairs, propertyByAccessor); - } - - [NotNull] - private static IEnumerable CreateMembers( - ITypeInstance typeInstance, - [NotNull] TypeDefinition typeDefinition, - DomainResolver domainResolver, - List<(MethodMember Member, MethodDefinition Definition)> methodPairs, - Dictionary propertyByAccessor - ) - { - var fieldMembers = typeDefinition - .Fields.Where(fieldDefinition => !fieldDefinition.IsBackingField()) - .Select(fieldDef => - (IMember)domainResolver.GetOrCreateFieldMember(typeInstance.Type, fieldDef) - ); - - var propertyMembers = typeDefinition.Properties.Select(propDef => - { - var propertyMember = CreatePropertyMember(typeInstance, propDef, domainResolver); - if (propDef.GetMethod != null) - { - propertyByAccessor[propDef.GetMethod] = propertyMember; - } - - if (propDef.SetMethod != null) - { - propertyByAccessor[propDef.SetMethod] = propertyMember; - } - - return (IMember)propertyMember; - }); - - var methodMembers = typeDefinition.Methods.Select(method => - { - var member = domainResolver - .GetOrCreateMethodMemberFromMethodReference(typeInstance, method) - .Member; - methodPairs.Add((member, method)); - return (IMember)member; - }); - - return fieldMembers - .Concat(propertyMembers) - .Concat(methodMembers) - .Where(member => !member.IsCompilerGenerated); - } - - [NotNull] - private static PropertyMember CreatePropertyMember( - ITypeInstance typeInstance, - PropertyDefinition propertyDefinition, - DomainResolver domainResolver - ) - { - var typeReference = propertyDefinition.PropertyType; - var propertyType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - typeReference - ); - var isCompilerGenerated = propertyDefinition.IsCompilerGenerated(); - var isStatic = - (propertyDefinition.SetMethod != null && propertyDefinition.SetMethod.IsStatic) - || (propertyDefinition.GetMethod != null && propertyDefinition.GetMethod.IsStatic); - var writeAccessor = GetPropertyWriteAccessor(propertyDefinition); - return new PropertyMember( - typeInstance.Type, - propertyDefinition.Name, - propertyDefinition.FullName, - propertyType, - isCompilerGenerated, - isStatic, - writeAccessor - ); - } - - private static Writability GetPropertyWriteAccessor( - [NotNull] PropertyDefinition propertyDefinition - ) - { - if (propertyDefinition.SetMethod == null) - { - return Writability.ReadOnly; - } - - if (CheckPropertyHasInitSetterInNetStandardCompatibleWay(propertyDefinition)) - { - return Writability.InitOnly; - } - - return Writability.Writable; - } - - private static bool CheckPropertyHasInitSetterInNetStandardCompatibleWay( - PropertyDefinition propertyDefinition - ) - { - return propertyDefinition.SetMethod?.ReturnType.IsRequiredModifier == true - && ((RequiredModifierType)propertyDefinition.SetMethod.ReturnType) - .ModifierType - .FullName == "System.Runtime.CompilerServices.IsExternalInit"; - } - } - - /// - /// Bundles the results of member creation: method (MethodMember, MethodDefinition) pairs - /// and a mapping from getter/setter MethodDefinitions to their PropertyMembers. - /// - internal sealed class MemberData - { - public MemberData( - IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs, - IReadOnlyDictionary propertyByAccessor - ) - { - MethodPairs = methodPairs; - PropertyByAccessor = propertyByAccessor; - } - - public IReadOnlyList<( - MethodMember Member, - MethodDefinition Definition - )> MethodPairs { get; } - public IReadOnlyDictionary PropertyByAccessor { get; } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs deleted file mode 100644 index a1c37745f..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs +++ /dev/null @@ -1,409 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; -using JetBrains.Annotations; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Resolves method signature and body dependencies for each method in the type, - /// including method calls, body types, cast types, type checks, metadata types, - /// and accessed fields. Also links getter/setter methods to their properties. - /// - internal static class AddMethodDependencies - { - internal static void Execute( - IType type, - DomainResolver domainResolver, - IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs, - IReadOnlyDictionary propertyByAccessor - ) - { - methodPairs - .Where(pair => pair.Member != null && !pair.Member.IsCompilerGenerated) - .Select(pair => - { - var dependencies = CreateMethodSignatureDependencies( - pair.Definition, - pair.Member, - domainResolver - ) - .Concat( - CreateMethodBodyDependencies( - type, - pair.Definition, - pair.Member, - domainResolver - ) - ); - if (pair.Definition.IsSetter || pair.Definition.IsGetter) - { - AssignDependenciesToProperty( - pair.Member, - pair.Definition, - propertyByAccessor - ); - } - - return (pair.Member, dependencies); - }) - .ForEach(tuple => - { - var (methodMember, dependencies) = tuple; - methodMember.MemberDependencies.AddRange(dependencies); - }); - } - - private static void AssignDependenciesToProperty( - MethodMember methodMember, - MethodDefinition methodDefinition, - IReadOnlyDictionary propertyByAccessor - ) - { - if (!propertyByAccessor.TryGetValue(methodDefinition, out var accessedProperty)) - { - return; - } - - accessedProperty.IsVirtual |= methodMember.IsVirtual; - - var methodForm = methodDefinition.GetMethodForm(); - switch (methodForm) - { - case MethodForm.Getter: - accessedProperty.Getter = methodMember; - break; - case MethodForm.Setter: - accessedProperty.Setter = methodMember; - break; - } - - var methodBody = methodDefinition.Body; - - if (methodBody == null) - { - return; - } - - if ( - !methodBody - .Instructions.Select(instruction => instruction.Operand) - .OfType() - .Any(definition => definition.IsBackingField()) - ) - { - accessedProperty.IsAutoProperty = false; - } - } - - [NotNull] - private static IEnumerable CreateMethodSignatureDependencies( - MethodReference methodReference, - MethodMember methodMember, - DomainResolver domainResolver - ) - { - var returnType = methodReference.GetReturnType(domainResolver); - return (returnType != null ? new[] { returnType } : Array.Empty>()) - .Concat(methodReference.GetParameters(domainResolver)) - .Concat(methodReference.GetGenericParameters(domainResolver)) - .Distinct() - .Select(signatureType => new MethodSignatureDependency( - methodMember, - signatureType - )); - } - - [NotNull] - private static IEnumerable CreateMethodBodyDependencies( - IType type, - MethodDefinition methodDefinition, - MethodMember methodMember, - DomainResolver domainResolver - ) - { - var methodBody = methodDefinition.Body; - if (methodBody == null) - { - yield break; - } - - var visitedMethodReferences = new List { methodDefinition }; - var bodyTypes = new List>(); - - if (methodDefinition.IsAsync()) - { - HandleAsync( - out methodDefinition, - ref methodBody, - bodyTypes, - visitedMethodReferences, - domainResolver - ); - } - - if (methodDefinition.IsIterator()) - { - HandleIterator( - out methodDefinition, - ref methodBody, - bodyTypes, - visitedMethodReferences, - domainResolver - ); - } - - var scan = methodDefinition.ScanMethodBody(domainResolver); - bodyTypes.AddRange(scan.BodyTypes); - - var castTypes = scan.CastTypes; - - var typeCheckTypes = scan.TypeCheckTypes; - - var metaDataTypes = scan.MetaDataTypes; - - var accessedFieldMembers = scan.AccessedFieldMembers; - - var calledMethodMembers = CreateMethodBodyDependenciesRecursive( - methodBody, - visitedMethodReferences, - bodyTypes, - castTypes, - typeCheckTypes, - metaDataTypes, - accessedFieldMembers, - domainResolver - ); - - foreach ( - var calledMethodMember in calledMethodMembers - .Where(method => !method.Member.IsCompilerGenerated) - .Distinct() - ) - { - yield return new MethodCallDependency(methodMember, calledMethodMember); - } - - foreach ( - var bodyType in bodyTypes - .Where(instance => !instance.Type.IsCompilerGenerated) - .Distinct() - ) - { - yield return new BodyTypeMemberDependency(methodMember, bodyType); - } - - foreach ( - var castType in castTypes - .Where(instance => !instance.Type.IsCompilerGenerated) - .Distinct() - ) - { - yield return new CastTypeDependency(methodMember, castType); - } - - foreach ( - var typeCheckType in typeCheckTypes - .Where(instance => !instance.Type.IsCompilerGenerated) - .Distinct() - ) - { - yield return new TypeCheckDependency(methodMember, typeCheckType); - } - - foreach ( - var metaDataType in metaDataTypes - .Where(instance => !instance.Type.IsCompilerGenerated) - .Distinct() - ) - { - yield return new MetaDataDependency(methodMember, metaDataType); - } - - foreach ( - var fieldMember in accessedFieldMembers - .Where(field => !field.IsCompilerGenerated) - .Distinct() - ) - { - yield return new AccessFieldDependency(methodMember, fieldMember); - } - } - - private static IEnumerable CreateMethodBodyDependenciesRecursive( - MethodBody methodBody, - ICollection visitedMethodReferences, - List> bodyTypes, - List> castTypes, - List> typeCheckTypes, - List> metaDataTypes, - List accessedFieldMembers, - DomainResolver domainResolver - ) - { - var calledMethodReferences = methodBody - .Instructions.Select(instruction => instruction.Operand) - .OfType(); - - foreach ( - var calledMethodReference in calledMethodReferences.Except(visitedMethodReferences) - ) - { - visitedMethodReferences.Add(calledMethodReference); - - var calledType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( - calledMethodReference.DeclaringType - ); - var calledMethodMember = domainResolver.GetOrCreateMethodMemberFromMethodReference( - calledType, - calledMethodReference - ); - - bodyTypes.AddRange(calledMethodMember.MemberGenericArguments); - - if (calledMethodReference.IsCompilerGenerated()) - { - MethodDefinition calledMethodDefinition; - try - { - calledMethodDefinition = calledMethodReference.Resolve(); - } - catch (AssemblyResolutionException) - { - calledMethodDefinition = null; - } - - if (calledMethodDefinition?.Body == null) - { - //MethodReference to compiler generated type not resolvable, skip - continue; - } - - var calledMethodBody = calledMethodDefinition.Body; - - if (calledMethodDefinition.IsIterator()) - { - HandleIterator( - out calledMethodDefinition, - ref calledMethodBody, - bodyTypes, - visitedMethodReferences, - domainResolver - ); - } - - var calledScan = calledMethodDefinition.ScanMethodBody(domainResolver); - bodyTypes.AddRange(calledScan.BodyTypes); - castTypes.AddRange(calledScan.CastTypes); - typeCheckTypes.AddRange(calledScan.TypeCheckTypes); - metaDataTypes.AddRange(calledScan.MetaDataTypes); - accessedFieldMembers.AddRange(calledScan.AccessedFieldMembers); - - foreach ( - var dep in CreateMethodBodyDependenciesRecursive( - calledMethodBody, - visitedMethodReferences, - bodyTypes, - castTypes, - typeCheckTypes, - metaDataTypes, - accessedFieldMembers, - domainResolver - ) - ) - { - yield return dep; - } - } - else - { - yield return calledMethodMember; - } - } - } - - private static void HandleIterator( - out MethodDefinition methodDefinition, - ref MethodBody methodBody, - List> bodyTypes, - ICollection visitedMethodReferences, - DomainResolver domainResolver - ) - { - var compilerGeneratedGeneratorObject = ( - (MethodReference)methodBody.Instructions.First(inst => inst.IsNewObjectOp()).Operand - ).DeclaringType.Resolve(); - methodDefinition = compilerGeneratedGeneratorObject.Methods.First(method => - method.Name == nameof(IEnumerator.MoveNext) - ); - visitedMethodReferences.Add(methodDefinition); - methodBody = methodDefinition.Body; - - var fieldsExceptGeneratorStateInfo = compilerGeneratedGeneratorObject.Fields.Where( - field => - !( - field.Name.EndsWith("__state") - || field.Name.EndsWith("__current") - || field.Name.EndsWith("__initialThreadId") - || field.Name.EndsWith("__this") - ) - ); - - bodyTypes.AddRange( - fieldsExceptGeneratorStateInfo.Select(bodyField => - domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) - ) - ); - } - - private static void HandleAsync( - out MethodDefinition methodDefinition, - ref MethodBody methodBody, - List> bodyTypes, - ICollection visitedMethodReferences, - DomainResolver domainResolver - ) - { - var compilerGeneratedGeneratorObject = ( - (MethodReference) - methodBody.Instructions.FirstOrDefault(inst => inst.IsNewObjectOp())?.Operand - )?.DeclaringType.Resolve(); - - if (compilerGeneratedGeneratorObject == null) - { - methodDefinition = methodBody.Method; - return; - } - - methodDefinition = compilerGeneratedGeneratorObject.Methods.First(method => - method.Name == nameof(IAsyncStateMachine.MoveNext) - ); - - visitedMethodReferences.Add(methodDefinition); - methodBody = methodDefinition.Body; - - var fieldsExceptGeneratorStateInfo = compilerGeneratedGeneratorObject - .Fields.Where(field => - !( - field.Name.EndsWith("__state") - || field.Name.EndsWith("__builder") - || field.Name.EndsWith("__this") - ) - ) - .ToArray(); - - bodyTypes.AddRange( - fieldsExceptGeneratorStateInfo.Select(bodyField => - domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) - ) - ); - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs b/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs deleted file mode 100644 index c2ce42182..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using ArchUnitNET.Domain; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Adds each type to its namespace's type list. Must run after all types have been - /// fully populated so that namespace queries return complete results. - /// - internal static class AddTypesToNamespaces - { - internal static void Execute(IEnumerable types) - { - foreach (var type in types) - { - ((List)type.Namespace.Types).Add(type); - } - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs b/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs deleted file mode 100644 index 0b3c7d9f9..000000000 --- a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Linq; -using ArchUnitNET.Domain; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - /// - /// Creates attribute instances from the assembly-level custom attributes of the - /// given and adds them to the domain - /// . - /// - internal static class CollectAssemblyAttributes - { - internal static void Execute( - Assembly assembly, - AssemblyDefinition assemblyDefinition, - DomainResolver domainResolver - ) - { - var attributeInstances = assemblyDefinition - .CustomAttributes.Select(attr => - attr.CreateAttributeFromCustomAttribute(domainResolver) - ) - .ToList(); - assembly.AttributeInstances.AddRange(attributeInstances); - } - } -} diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs new file mode 100644 index 000000000..e04e658b1 --- /dev/null +++ b/ArchUnitNET/Loader/TypeProcessor.cs @@ -0,0 +1,1138 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using ArchUnitNET.Domain; +using ArchUnitNET.Domain.Dependencies; +using ArchUnitNET.Domain.Extensions; +using JetBrains.Annotations; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace ArchUnitNET.Loader +{ + /// + /// Contains all type-processing phases that populate the domain model with members, + /// dependencies, and attributes. Each public method corresponds to a numbered phase + /// and must be called in the required order (see ). + /// + internal static class TypeProcessor + { + // ── Phase 1: Base class dependency ────────────────────────────────── + + /// + /// If the type has a base class, creates an + /// and adds it to the type's dependency list. Skipped for interfaces and types + /// whose base is not a . + /// + internal static void AddBaseClassDependency( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver + ) + { + var baseTypeRef = typeDefinition.BaseType; + if (baseTypeRef == null) + { + return; + } + + var baseType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(baseTypeRef); + if (!(baseType.Type is Class baseClass)) + { + return; + } + + var dependency = new InheritsBaseClassDependency( + type, + new TypeInstance( + baseClass, + baseType.GenericArguments, + baseType.ArrayDimensions + ) + ); + type.Dependencies.Add(dependency); + } + + // ── Phase 2: Members ──────────────────────────────────────────────── + + /// + /// Creates field, property, and method members from the Cecil + /// and adds them to the domain type. + /// Returns a containing method pairs and + /// a mapping from getter/setter s to + /// their s. + /// + internal static MemberData AddMembers( + ITypeInstance typeInstance, + TypeDefinition typeDefinition, + DomainResolver domainResolver + ) + { + var methodPairs = new List<(MethodMember Member, MethodDefinition Definition)>(); + var propertyByAccessor = new Dictionary(); + var members = CreateMembers( + typeInstance, + typeDefinition, + domainResolver, + methodPairs, + propertyByAccessor + ); + typeInstance.Type.Members.AddRange(members); + return new MemberData(methodPairs, propertyByAccessor); + } + + [NotNull] + private static IEnumerable CreateMembers( + ITypeInstance typeInstance, + [NotNull] TypeDefinition typeDefinition, + DomainResolver domainResolver, + List<(MethodMember Member, MethodDefinition Definition)> methodPairs, + Dictionary propertyByAccessor + ) + { + var fieldMembers = typeDefinition + .Fields.Where(fieldDefinition => !fieldDefinition.IsBackingField()) + .Select(fieldDef => + (IMember)domainResolver.GetOrCreateFieldMember(typeInstance.Type, fieldDef) + ); + + var propertyMembers = typeDefinition.Properties.Select(propDef => + { + var propertyMember = CreatePropertyMember(typeInstance, propDef, domainResolver); + if (propDef.GetMethod != null) + { + propertyByAccessor[propDef.GetMethod] = propertyMember; + } + + if (propDef.SetMethod != null) + { + propertyByAccessor[propDef.SetMethod] = propertyMember; + } + + return (IMember)propertyMember; + }); + + var methodMembers = typeDefinition.Methods.Select(method => + { + var member = domainResolver + .GetOrCreateMethodMemberFromMethodReference(typeInstance, method) + .Member; + methodPairs.Add((member, method)); + return (IMember)member; + }); + + return fieldMembers + .Concat(propertyMembers) + .Concat(methodMembers) + .Where(member => !member.IsCompilerGenerated); + } + + [NotNull] + private static PropertyMember CreatePropertyMember( + ITypeInstance typeInstance, + PropertyDefinition propertyDefinition, + DomainResolver domainResolver + ) + { + var typeReference = propertyDefinition.PropertyType; + var propertyType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + typeReference + ); + var isCompilerGenerated = propertyDefinition.IsCompilerGenerated(); + var isStatic = + (propertyDefinition.SetMethod != null && propertyDefinition.SetMethod.IsStatic) + || (propertyDefinition.GetMethod != null && propertyDefinition.GetMethod.IsStatic); + var writeAccessor = GetPropertyWriteAccessor(propertyDefinition); + return new PropertyMember( + typeInstance.Type, + propertyDefinition.Name, + propertyDefinition.FullName, + propertyType, + isCompilerGenerated, + isStatic, + writeAccessor + ); + } + + private static Writability GetPropertyWriteAccessor( + [NotNull] PropertyDefinition propertyDefinition + ) + { + if (propertyDefinition.SetMethod == null) + { + return Writability.ReadOnly; + } + + if (CheckPropertyHasInitSetterInNetStandardCompatibleWay(propertyDefinition)) + { + return Writability.InitOnly; + } + + return Writability.Writable; + } + + private static bool CheckPropertyHasInitSetterInNetStandardCompatibleWay( + PropertyDefinition propertyDefinition + ) + { + return propertyDefinition.SetMethod?.ReturnType.IsRequiredModifier == true + && ((RequiredModifierType)propertyDefinition.SetMethod.ReturnType) + .ModifierType + .FullName == "System.Runtime.CompilerServices.IsExternalInit"; + } + + // ── Phase 3: Generic parameter dependencies ───────────────────────── + + /// + /// Assigns declarers to generic parameters and creates type-constraint dependencies + /// for both type-level and member-level generic parameters. + /// + internal static void AddGenericParameterDependencies(IType type) + { + // Type-level generic parameters + foreach (var genericParameter in type.GenericParameters) + { + genericParameter.AssignDeclarer(type); + foreach (var typeInstanceConstraint in genericParameter.TypeInstanceConstraints) + { + var dependency = new TypeGenericParameterTypeConstraintDependency( + genericParameter, + typeInstanceConstraint + ); + genericParameter.Dependencies.Add(dependency); + } + } + + // Member-level generic parameters + foreach (var member in type.Members) + { + foreach (var genericParameter in member.GenericParameters) + { + genericParameter.AssignDeclarer(member); + foreach (var typeInstanceConstraint in genericParameter.TypeInstanceConstraints) + { + var dependency = new MemberGenericParameterTypeConstraintDependency( + genericParameter, + typeInstanceConstraint + ); + genericParameter.Dependencies.Add(dependency); + } + } + } + } + + // ── Phase 4: Attributes and attribute dependencies ────────────────── + + /// + /// Creates attribute instances from Cecil custom attributes on the type, its generic + /// parameters, and its members, then adds corresponding attribute-type dependencies. + /// + internal static void AddAttributesAndAttributeDependencies( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver, + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs + ) + { + typeDefinition.CustomAttributes.ForEach(attr => + AddAttributeArgumentReferenceDependencies(type, attr, domainResolver) + ); + var typeAttributeInstances = CreateAttributesFromCustomAttributes( + typeDefinition.CustomAttributes, + domainResolver + ) + .ToList(); + type.AttributeInstances.AddRange(typeAttributeInstances); + var typeAttributeDependencies = typeAttributeInstances.Select( + attributeInstance => new AttributeTypeDependency(type, attributeInstance) + ); + type.Dependencies.AddRange(typeAttributeDependencies); + + SetUpAttributesForTypeGenericParameters(type, typeDefinition, domainResolver); + CollectAttributesForMembers(type, typeDefinition, domainResolver, methodPairs); + } + + private static void SetUpAttributesForTypeGenericParameters( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver + ) + { + foreach (var genericParameter in typeDefinition.GenericParameters) + { + var param = type.GenericParameters.First(parameter => + parameter.Name == genericParameter.Name + ); + var attributeInstances = CreateAttributesFromCustomAttributes( + genericParameter.CustomAttributes, + domainResolver + ) + .ToList(); + type.AttributeInstances.AddRange(attributeInstances); + param.AttributeInstances.AddRange(attributeInstances); + var genericParameterAttributeDependencies = attributeInstances.Select( + attributeInstance => new AttributeTypeDependency(type, attributeInstance) + ); + type.Dependencies.AddRange(genericParameterAttributeDependencies); + } + } + + private static void CollectAttributesForMembers( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver, + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs + ) + { + typeDefinition + .Fields.Where(x => !x.IsBackingField() && !x.IsCompilerGenerated()) + .ForEach(fieldDef => SetUpAttributesForField(type, fieldDef, domainResolver)); + + typeDefinition + .Properties.Where(x => !x.IsCompilerGenerated()) + .ForEach(propDef => SetUpAttributesForProperty(type, propDef, domainResolver)); + + // Build a lookup from method full-name -> MethodMember to avoid O(n) scans + var methodMemberByFullName = new Dictionary(methodPairs.Count); + foreach (var pair in methodPairs) + { + var key = pair.Definition.BuildFullName(); + if (!methodMemberByFullName.ContainsKey(key)) + { + methodMemberByFullName[key] = pair.Member; + } + } + + typeDefinition + .Methods.Where(x => !x.IsCompilerGenerated()) + .ForEach(methodDef => + { + MethodMember methodMember; + if ( + !methodMemberByFullName.TryGetValue( + methodDef.BuildFullName(), + out methodMember + ) + ) + { + return; + } + + SetUpAttributesForMethod(methodDef, methodMember, type, domainResolver); + }); + } + + private static void SetUpAttributesForField( + IType type, + FieldDefinition fieldDefinition, + DomainResolver domainResolver + ) + { + var fieldMember = type.GetFieldMembers() + .WhereFullNameIs(fieldDefinition.FullName) + .RequiredNotNull(); + CollectMemberAttributesAndDependencies( + type, + fieldMember, + fieldDefinition.CustomAttributes.ToList(), + fieldMember.MemberDependencies, + domainResolver + ); + } + + private static void SetUpAttributesForProperty( + IType type, + PropertyDefinition propertyDefinition, + DomainResolver domainResolver + ) + { + var propertyMember = type.GetPropertyMembers() + .WhereFullNameIs(propertyDefinition.FullName) + .RequiredNotNull(); + CollectMemberAttributesAndDependencies( + type, + propertyMember, + propertyDefinition.CustomAttributes.ToList(), + propertyMember.AttributeDependencies, + domainResolver + ); + } + + private static void SetUpAttributesForMethod( + MethodDefinition methodDefinition, + MethodMember methodMember, + IType type, + DomainResolver domainResolver + ) + { + var memberCustomAttributes = methodDefinition.GetAllMethodCustomAttributes().ToList(); + SetUpAttributesForMethodGenericParameters( + methodDefinition, + methodMember, + domainResolver + ); + CollectMemberAttributesAndDependencies( + type, + methodMember, + memberCustomAttributes, + methodMember.MemberDependencies, + domainResolver + ); + } + + private static void SetUpAttributesForMethodGenericParameters( + MethodDefinition methodDefinition, + MethodMember methodMember, + DomainResolver domainResolver + ) + { + foreach (var genericParameter in methodDefinition.GenericParameters) + { + var param = methodMember.GenericParameters.First(parameter => + parameter.Name == genericParameter.Name + ); + var customAttributes = genericParameter.CustomAttributes; + customAttributes.ForEach(attr => + AddAttributeArgumentReferenceDependencies( + methodMember.DeclaringType, + attr, + domainResolver + ) + ); + var attributeInstances = CreateAttributesFromCustomAttributes( + customAttributes, + domainResolver + ) + .ToList(); + methodMember.AttributeInstances.AddRange(attributeInstances); + param.AttributeInstances.AddRange(attributeInstances); + var genericParameterAttributeDependencies = attributeInstances.Select( + attributeInstance => new AttributeMemberDependency( + methodMember, + attributeInstance + ) + ); + methodMember.MemberDependencies.AddRange(genericParameterAttributeDependencies); + } + } + + private static void CollectMemberAttributesAndDependencies( + IType type, + IMember member, + List memberCustomAttributes, + List attributeDependencies, + DomainResolver domainResolver + ) + { + memberCustomAttributes.ForEach(attr => + AddAttributeArgumentReferenceDependencies(type, attr, domainResolver) + ); + var memberAttributeInstances = CreateAttributesFromCustomAttributes( + memberCustomAttributes, + domainResolver + ) + .ToList(); + member.AttributeInstances.AddRange(memberAttributeInstances); + var methodAttributeDependencies = memberAttributeInstances.Select( + attributeInstance => new AttributeMemberDependency(member, attributeInstance) + ); + attributeDependencies.AddRange(methodAttributeDependencies); + } + + [NotNull] + private static IEnumerable CreateAttributesFromCustomAttributes( + IEnumerable customAttributes, + DomainResolver domainResolver + ) + { + return customAttributes + .Where(customAttribute => + customAttribute.AttributeType.FullName + != "Microsoft.CodeAnalysis.EmbeddedAttribute" + && customAttribute.AttributeType.FullName + != "System.Runtime.CompilerServices.NullableAttribute" + && customAttribute.AttributeType.FullName + != "System.Runtime.CompilerServices.NullableContextAttribute" + ) + .Select(attr => attr.CreateAttributeFromCustomAttribute(domainResolver)); + } + + private static void AddAttributeArgumentReferenceDependencies( + IType type, + ICustomAttribute customAttribute, + DomainResolver domainResolver + ) + { + if (!customAttribute.HasConstructorArguments) + { + return; + } + + var attributeConstructorArgs = customAttribute.ConstructorArguments; + attributeConstructorArgs + .Where(attributeArgument => + attributeArgument.Value is TypeReference typeReference + && !typeReference.IsCompilerGenerated() + ) + .Select(attributeArgument => + (typeReference: attributeArgument.Value as TypeReference, attributeArgument) + ) + .ForEach(tuple => + { + var argumentType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + tuple.typeReference + ); + var dependency = new TypeReferenceDependency(type, argumentType); + type.Dependencies.Add(dependency); + }); + } + + // ── Phase 5: Assembly-level attributes ────────────────────────────── + + /// + /// Creates attribute instances from the assembly-level custom attributes of the + /// given and adds them to the domain + /// . + /// + internal static void CollectAssemblyAttributes( + Assembly assembly, + AssemblyDefinition assemblyDefinition, + DomainResolver domainResolver + ) + { + var attributeInstances = assemblyDefinition + .CustomAttributes.Select(attr => + attr.CreateAttributeFromCustomAttribute(domainResolver) + ) + .ToList(); + assembly.AttributeInstances.AddRange(attributeInstances); + } + + // ── Phase 6: Field and property type dependencies ─────────────────── + + /// + /// Creates and + /// instances for each field and property member of the type. + /// + internal static void AddFieldAndPropertyDependencies(IType type) + { + type.GetFieldMembers() + .ForEach(field => + { + var dependency = new FieldTypeDependency(field); + if (!field.MemberDependencies.Contains(dependency)) + { + field.MemberDependencies.Add(dependency); + } + }); + + type.GetPropertyMembers() + .ForEach(property => + { + var dependency = new PropertyTypeDependency(property); + if (!property.MemberDependencies.Contains(dependency)) + { + property.MemberDependencies.Add(dependency); + } + }); + } + + // ── Phase 7: Method signature and body dependencies ───────────────── + + /// + /// Resolves method signature and body dependencies for each method in the type, + /// including method calls, body types, cast types, type checks, metadata types, + /// and accessed fields. Also links getter/setter methods to their properties. + /// + internal static void AddMethodDependencies( + IType type, + DomainResolver domainResolver, + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs, + IReadOnlyDictionary propertyByAccessor + ) + { + methodPairs + .Where(pair => pair.Member != null && !pair.Member.IsCompilerGenerated) + .Select(pair => + { + var dependencies = CreateMethodSignatureDependencies( + pair.Definition, + pair.Member, + domainResolver + ) + .Concat( + CreateMethodBodyDependencies( + type, + pair.Definition, + pair.Member, + domainResolver + ) + ); + if (pair.Definition.IsSetter || pair.Definition.IsGetter) + { + AssignDependenciesToProperty( + pair.Member, + pair.Definition, + propertyByAccessor + ); + } + + return (pair.Member, dependencies); + }) + .ForEach(tuple => + { + var (methodMember, dependencies) = tuple; + methodMember.MemberDependencies.AddRange(dependencies); + }); + } + + private static void AssignDependenciesToProperty( + MethodMember methodMember, + MethodDefinition methodDefinition, + IReadOnlyDictionary propertyByAccessor + ) + { + if (!propertyByAccessor.TryGetValue(methodDefinition, out var accessedProperty)) + { + return; + } + + accessedProperty.IsVirtual |= methodMember.IsVirtual; + + var methodForm = methodDefinition.GetMethodForm(); + switch (methodForm) + { + case MethodForm.Getter: + accessedProperty.Getter = methodMember; + break; + case MethodForm.Setter: + accessedProperty.Setter = methodMember; + break; + } + + var methodBody = methodDefinition.Body; + + if (methodBody == null) + { + return; + } + + if ( + !methodBody + .Instructions.Select(instruction => instruction.Operand) + .OfType() + .Any(definition => definition.IsBackingField()) + ) + { + accessedProperty.IsAutoProperty = false; + } + } + + [NotNull] + private static IEnumerable CreateMethodSignatureDependencies( + MethodReference methodReference, + MethodMember methodMember, + DomainResolver domainResolver + ) + { + var returnType = methodReference.GetReturnType(domainResolver); + return (returnType != null ? new[] { returnType } : Array.Empty>()) + .Concat(methodReference.GetParameters(domainResolver)) + .Concat(methodReference.GetGenericParameters(domainResolver)) + .Distinct() + .Select(signatureType => new MethodSignatureDependency( + methodMember, + signatureType + )); + } + + [NotNull] + private static IEnumerable CreateMethodBodyDependencies( + IType type, + MethodDefinition methodDefinition, + MethodMember methodMember, + DomainResolver domainResolver + ) + { + var methodBody = methodDefinition.Body; + if (methodBody == null) + { + yield break; + } + + var visitedMethodReferences = new List { methodDefinition }; + var bodyTypes = new List>(); + + if (methodDefinition.IsAsync()) + { + HandleAsync( + out methodDefinition, + ref methodBody, + bodyTypes, + visitedMethodReferences, + domainResolver + ); + } + + if (methodDefinition.IsIterator()) + { + HandleIterator( + out methodDefinition, + ref methodBody, + bodyTypes, + visitedMethodReferences, + domainResolver + ); + } + + var scan = methodDefinition.ScanMethodBody(domainResolver); + bodyTypes.AddRange(scan.BodyTypes); + + var castTypes = scan.CastTypes; + + var typeCheckTypes = scan.TypeCheckTypes; + + var metaDataTypes = scan.MetaDataTypes; + + var accessedFieldMembers = scan.AccessedFieldMembers; + + var calledMethodMembers = CreateMethodBodyDependenciesRecursive( + methodBody, + visitedMethodReferences, + bodyTypes, + castTypes, + typeCheckTypes, + metaDataTypes, + accessedFieldMembers, + domainResolver + ); + + foreach ( + var calledMethodMember in calledMethodMembers + .Where(method => !method.Member.IsCompilerGenerated) + .Distinct() + ) + { + yield return new MethodCallDependency(methodMember, calledMethodMember); + } + + foreach ( + var bodyType in bodyTypes + .Where(instance => !instance.Type.IsCompilerGenerated) + .Distinct() + ) + { + yield return new BodyTypeMemberDependency(methodMember, bodyType); + } + + foreach ( + var castType in castTypes + .Where(instance => !instance.Type.IsCompilerGenerated) + .Distinct() + ) + { + yield return new CastTypeDependency(methodMember, castType); + } + + foreach ( + var typeCheckType in typeCheckTypes + .Where(instance => !instance.Type.IsCompilerGenerated) + .Distinct() + ) + { + yield return new TypeCheckDependency(methodMember, typeCheckType); + } + + foreach ( + var metaDataType in metaDataTypes + .Where(instance => !instance.Type.IsCompilerGenerated) + .Distinct() + ) + { + yield return new MetaDataDependency(methodMember, metaDataType); + } + + foreach ( + var fieldMember in accessedFieldMembers + .Where(field => !field.IsCompilerGenerated) + .Distinct() + ) + { + yield return new AccessFieldDependency(methodMember, fieldMember); + } + } + + private static IEnumerable CreateMethodBodyDependenciesRecursive( + MethodBody methodBody, + ICollection visitedMethodReferences, + List> bodyTypes, + List> castTypes, + List> typeCheckTypes, + List> metaDataTypes, + List accessedFieldMembers, + DomainResolver domainResolver + ) + { + var calledMethodReferences = methodBody + .Instructions.Select(instruction => instruction.Operand) + .OfType(); + + foreach ( + var calledMethodReference in calledMethodReferences.Except(visitedMethodReferences) + ) + { + visitedMethodReferences.Add(calledMethodReference); + + var calledType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + calledMethodReference.DeclaringType + ); + var calledMethodMember = domainResolver.GetOrCreateMethodMemberFromMethodReference( + calledType, + calledMethodReference + ); + + bodyTypes.AddRange(calledMethodMember.MemberGenericArguments); + + if (calledMethodReference.IsCompilerGenerated()) + { + MethodDefinition calledMethodDefinition; + try + { + calledMethodDefinition = calledMethodReference.Resolve(); + } + catch (AssemblyResolutionException) + { + calledMethodDefinition = null; + } + + if (calledMethodDefinition?.Body == null) + { + //MethodReference to compiler generated type not resolvable, skip + continue; + } + + var calledMethodBody = calledMethodDefinition.Body; + + if (calledMethodDefinition.IsIterator()) + { + HandleIterator( + out calledMethodDefinition, + ref calledMethodBody, + bodyTypes, + visitedMethodReferences, + domainResolver + ); + } + + var calledScan = calledMethodDefinition.ScanMethodBody(domainResolver); + bodyTypes.AddRange(calledScan.BodyTypes); + castTypes.AddRange(calledScan.CastTypes); + typeCheckTypes.AddRange(calledScan.TypeCheckTypes); + metaDataTypes.AddRange(calledScan.MetaDataTypes); + accessedFieldMembers.AddRange(calledScan.AccessedFieldMembers); + + foreach ( + var dep in CreateMethodBodyDependenciesRecursive( + calledMethodBody, + visitedMethodReferences, + bodyTypes, + castTypes, + typeCheckTypes, + metaDataTypes, + accessedFieldMembers, + domainResolver + ) + ) + { + yield return dep; + } + } + else + { + yield return calledMethodMember; + } + } + } + + private static void HandleIterator( + out MethodDefinition methodDefinition, + ref MethodBody methodBody, + List> bodyTypes, + ICollection visitedMethodReferences, + DomainResolver domainResolver + ) + { + var compilerGeneratedGeneratorObject = ( + (MethodReference)methodBody.Instructions.First(inst => inst.IsNewObjectOp()).Operand + ).DeclaringType.Resolve(); + methodDefinition = compilerGeneratedGeneratorObject.Methods.First(method => + method.Name == nameof(IEnumerator.MoveNext) + ); + visitedMethodReferences.Add(methodDefinition); + methodBody = methodDefinition.Body; + + var fieldsExceptGeneratorStateInfo = compilerGeneratedGeneratorObject.Fields.Where( + field => + !( + field.Name.EndsWith("__state") + || field.Name.EndsWith("__current") + || field.Name.EndsWith("__initialThreadId") + || field.Name.EndsWith("__this") + ) + ); + + bodyTypes.AddRange( + fieldsExceptGeneratorStateInfo.Select(bodyField => + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) + ) + ); + } + + private static void HandleAsync( + out MethodDefinition methodDefinition, + ref MethodBody methodBody, + List> bodyTypes, + ICollection visitedMethodReferences, + DomainResolver domainResolver + ) + { + var compilerGeneratedGeneratorObject = ( + (MethodReference) + methodBody.Instructions.FirstOrDefault(inst => inst.IsNewObjectOp())?.Operand + )?.DeclaringType.Resolve(); + + if (compilerGeneratedGeneratorObject == null) + { + methodDefinition = methodBody.Method; + return; + } + + methodDefinition = compilerGeneratedGeneratorObject.Methods.First(method => + method.Name == nameof(IAsyncStateMachine.MoveNext) + ); + + visitedMethodReferences.Add(methodDefinition); + methodBody = methodDefinition.Body; + + var fieldsExceptGeneratorStateInfo = compilerGeneratedGeneratorObject + .Fields.Where(field => + !( + field.Name.EndsWith("__state") + || field.Name.EndsWith("__builder") + || field.Name.EndsWith("__this") + ) + ) + .ToArray(); + + bodyTypes.AddRange( + fieldsExceptGeneratorStateInfo.Select(bodyField => + domainResolver.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) + ) + ); + } + + // ── Phase 8: Generic argument dependencies ────────────────────────── + + /// + /// Propagates generic-parameter dependencies to the owning type and member, + /// then recursively discovers nested generic-argument dependencies in all + /// existing type and member dependencies. + /// + internal static void AddGenericArgumentDependencies(IType type) + { + // Type-level generic argument dependencies + foreach (var parameter in type.GenericParameters) + { + type.Dependencies.AddRange(parameter.Dependencies); + } + + // Member-level generic argument dependencies + foreach (var member in type.Members) + { + foreach (var parameter in member.GenericParameters) + { + member.MemberDependencies.AddRange( + parameter.Dependencies.Cast() + ); + } + } + + // Recursive generic arguments in type dependencies + var typeDependencies = new List(); + foreach (var dependency in type.Dependencies) + { + FindGenericArgumentsInTypeDependenciesRecursive( + type, + dependency.TargetGenericArguments, + typeDependencies + ); + } + + type.Dependencies.AddRange(typeDependencies); + + // Recursive generic arguments in member dependencies + foreach (var member in type.Members) + { + var memberDependencies = new List(); + foreach (var dependency in member.Dependencies) + { + FindGenericArgumentsInMemberDependenciesRecursive( + member, + dependency.TargetGenericArguments, + memberDependencies + ); + } + + member.MemberDependencies.AddRange(memberDependencies); + } + } + + private static void FindGenericArgumentsInTypeDependenciesRecursive( + IType type, + IEnumerable targetGenericArguments, + ICollection createdDependencies + ) + { + foreach ( + var genericArgument in targetGenericArguments.Where(argument => + !argument.Type.IsGenericParameter + ) + ) + { + createdDependencies.Add(new GenericArgumentTypeDependency(type, genericArgument)); + FindGenericArgumentsInTypeDependenciesRecursive( + type, + genericArgument.GenericArguments, + createdDependencies + ); + } + } + + private static void FindGenericArgumentsInMemberDependenciesRecursive( + IMember member, + IEnumerable targetGenericArguments, + ICollection createdDependencies + ) + { + foreach ( + var genericArgument in targetGenericArguments.Where(argument => + !argument.Type.IsGenericParameter + ) + ) + { + createdDependencies.Add( + new GenericArgumentMemberDependency(member, genericArgument) + ); + FindGenericArgumentsInMemberDependenciesRecursive( + member, + genericArgument.GenericArguments, + createdDependencies + ); + } + } + + // ── Phase 9: Interface and member-to-type dependencies ────────────── + + /// + /// Creates for each interface the type + /// implements (including inherited interfaces), then rolls up all member-level + /// dependencies to the type's dependency list. + /// + internal static void AddClassDependencies( + IType type, + TypeDefinition typeDefinition, + DomainResolver domainResolver + ) + { + // Interface dependencies + GetInterfacesImplementedByClass(typeDefinition) + .ForEach(target => + { + var targetType = domainResolver.GetOrCreateStubTypeInstanceFromTypeReference( + target + ); + type.Dependencies.Add(new ImplementsInterfaceDependency(type, targetType)); + }); + + // Member dependencies rolled up to type level + type.Members.ForEach(member => + { + type.Dependencies.AddRange(member.Dependencies); + }); + } + + private static IEnumerable GetInterfacesImplementedByClass( + TypeDefinition typeDefinition + ) + { + var baseType = typeDefinition.BaseType?.Resolve(); + var baseInterfaces = + baseType != null + ? GetInterfacesImplementedByClass(baseType) + : new List(); + + return typeDefinition + .Interfaces.Select(implementation => implementation.InterfaceType) + .Concat(baseInterfaces); + } + + // ── Phase 10: Backwards dependencies ──────────────────────────────── + + /// + /// Registers each of the type's dependencies as a backwards dependency on the + /// target type, and each member-member dependency as a backwards dependency on + /// the target member. + /// + internal static void AddBackwardsDependencies(IType type) + { + type.Dependencies.ForEach(dependency => + dependency.Target.BackwardsDependencies.Add(dependency) + ); + + var memberMemberDependencies = type + .Members.SelectMany(member => member.MemberDependencies) + .OfType(); + memberMemberDependencies.ForEach(memberDependency => + memberDependency.TargetMember.MemberBackwardsDependencies.Add(memberDependency) + ); + } + + // ── Phase 11: Register types with namespaces ──────────────────────── + + /// + /// Adds each type to its namespace's type list. Must run after all types have been + /// fully populated so that namespace queries return complete results. + /// + internal static void AddTypesToNamespaces(IEnumerable types) + { + foreach (var type in types) + { + ((List)type.Namespace.Types).Add(type); + } + } + } + + /// + /// Bundles the results of member creation: method (MethodMember, MethodDefinition) pairs + /// and a mapping from getter/setter MethodDefinitions to their PropertyMembers. + /// + internal sealed class MemberData + { + public MemberData( + IReadOnlyList<(MethodMember Member, MethodDefinition Definition)> methodPairs, + IReadOnlyDictionary propertyByAccessor + ) + { + MethodPairs = methodPairs; + PropertyByAccessor = propertyByAccessor; + } + + public IReadOnlyList<( + MethodMember Member, + MethodDefinition Definition + )> MethodPairs { get; } + public IReadOnlyDictionary PropertyByAccessor { get; } + } +} From d54bc8e09d113d02fcd8b8ebe05f4bccb496d0ba Mon Sep 17 00:00:00 2001 From: Alexander Linne Date: Fri, 27 Mar 2026 15:24:20 +0100 Subject: [PATCH 15/15] docs: update code comment in guide.md Signed-off-by: Alexander Linne --- documentation/docs/guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/guide.md b/documentation/docs/guide.md index b9c4cc63a..8d75c21b3 100644 --- a/documentation/docs/guide.md +++ b/documentation/docs/guide.md @@ -51,7 +51,7 @@ ArchUnitNET uses two levels of caching to improve performance: Both caches are enabled by default. You can configure them via `ArchLoader`: ```cs -// Disable rule evaluation caching (every rule evaluates its providers independently) +// Disable both rule evaluation and architecture caching private static readonly Architecture Architecture = new ArchLoader() .WithoutRuleEvaluationCache()