diff --git a/ArchUnitNET/Domain/Architecture.cs b/ArchUnitNET/Domain/Architecture.cs index 2e4cc0ddb..bbeb130ed 100644 --- a/ArchUnitNET/Domain/Architecture.cs +++ b/ArchUnitNET/Domain/Architecture.cs @@ -1,20 +1,45 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; 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 ObjectProviderCache _objectProviderCache; + 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; @@ -22,7 +47,7 @@ IEnumerable referencedTypes Types = types; GenericParameters = genericParameters; ReferencedTypes = referencedTypes; - _objectProviderCache = new ObjectProviderCache(this); + _useRuleEvaluationCache = useRuleEvaluationCache; } public IEnumerable Assemblies => @@ -44,13 +69,28 @@ 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 { - return _objectProviderCache.GetOrCreateObjects(objectProvider, providingFunction); + if (!_useRuleEvaluationCache) + { + return providingFunction(this); + } + 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/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/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)); - } - } - } -} diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index db9af6a0d..9df3c9df8 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -2,63 +2,88 @@ 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; namespace ArchUnitNET.Loader { + /// + /// Internal builder that constructs an from loaded modules. + /// Coordinates type discovery (via ), type processing + /// (via ), and architecture assembly. Manages the + /// lookup and supports disabling rule-evaluation caching. + /// internal class ArchBuilder { private readonly ArchitectureCache _architectureCache; private readonly ArchitectureCacheKey _architectureCacheKey; - private readonly IDictionary _architectureTypes = - new Dictionary(); - private readonly AssemblyRegistry _assemblyRegistry; - private readonly LoadTaskRegistry _loadTaskRegistry; - private readonly NamespaceRegistry _namespaceRegistry; - private readonly TypeFactory _typeFactory; + 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() { - _assemblyRegistry = new AssemblyRegistry(); - _namespaceRegistry = new NamespaceRegistry(); - _loadTaskRegistry = new LoadTaskRegistry(); - _typeFactory = new TypeFactory( - _loadTaskRegistry, - _assemblyRegistry, - _namespaceRegistry - ); + _domainResolver = new DomainResolver(); _architectureCacheKey = new ArchitectureCacheKey(); _architectureCache = ArchitectureCache.Instance; } - public IEnumerable Types => _architectureTypes.Values; - public IEnumerable Assemblies => _assemblyRegistry.Assemblies; - public IEnumerable Namespaces => _namespaceRegistry.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 (!_assemblyRegistry.ContainsAssembly(moduleAssembly.FullName)) - { - var assembly = _assemblyRegistry.GetOrCreateAssembly( - moduleAssembly.Name.Name, - moduleAssembly.FullName, - isOnlyReferenced, - references - ); - _loadTaskRegistry.Add( - typeof(CollectAssemblyAttributes), - new CollectAssemblyAttributes(assembly, moduleAssembly, _typeFactory) - ); - } + 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); @@ -87,10 +112,12 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) types.AddRange(nestedTypes); } - 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" @@ -98,68 +125,186 @@ public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter) ) .ForEach(typeDefinition => { - var type = _typeFactory.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() + /// + /// Builds the from all loaded modules. Returns a cached + /// instance when available (unless is set). + /// + public Architecture Build(bool skipRuleEvaluationCache, bool skipArchitectureCache) { - _loadTaskRegistry.ExecuteTasks( - new List - { - typeof(AddMembers), - typeof(AddGenericParameterDependencies), - typeof(AddAttributesAndAttributeDependencies), - typeof(CollectAssemblyAttributes), - typeof(AddFieldAndPropertyDependencies), - typeof(AddMethodDependencies), - typeof(AddGenericArgumentDependencies), - typeof(AddClassDependencies), - typeof(AddBackwardsDependencies), - typeof(AddTypesToNamespaces), - } - ); - } + if (skipRuleEvaluationCache) + { + _architectureCacheKey.SetRuleEvaluationCacheDisabled(); + } - public Architecture Build() - { - var architecture = _architectureCache.TryGetArchitecture(_architectureCacheKey); - if (architecture != null) + if (!skipArchitectureCache) { - return architecture; + var architecture = _architectureCache.TryGetArchitecture(_architectureCacheKey); + if (architecture != null) + { + return architecture; + } } - UpdateTypeDefinitions(); - var allTypes = _typeFactory.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 + referencedTypes, + !skipRuleEvaluationCache ); - _architectureCache.Add(_architectureCacheKey, newArchitecture); + + if (!skipArchitectureCache) + { + _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)) + { + TypeProcessor.AddBaseClassDependency( + 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 = TypeProcessor.AddMembers( + entry.TypeInstance, + entry.Definition, + _domainResolver + ) + select (entry.TypeInstance, entry.Definition, memberData) + ); + + // Phase 3: Generic parameter dependencies + foreach (var entry in typesToProcess) + { + TypeProcessor.AddGenericParameterDependencies(entry.TypeInstance.Type); + } + + // Phase 4: Attributes and attribute dependencies + foreach (var entry in typesWithMemberData) + { + TypeProcessor.AddAttributesAndAttributeDependencies( + 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) + { + TypeProcessor.CollectAssemblyAttributes( + entry.Assembly, + entry.Definition, + _domainResolver + ); + } + + // Phase 6: Field and property type dependencies + foreach (var entry in typesToProcess) + { + TypeProcessor.AddFieldAndPropertyDependencies(entry.TypeInstance.Type); + } + + // Phase 7: Method signature and body dependencies + foreach (var entry in typesWithMemberData) + { + TypeProcessor.AddMethodDependencies( + entry.TypeInstance.Type, + _domainResolver, + entry.MemberData.MethodPairs, + entry.MemberData.PropertyByAccessor + ); + } + + // Phase 8: Generic argument dependencies + foreach (var entry in typesToProcess) + { + TypeProcessor.AddGenericArgumentDependencies(entry.TypeInstance.Type); + } + + // Phase 9: Interface and member-to-type dependencies + foreach (var entry in typesToProcess) + { + TypeProcessor.AddClassDependencies( + entry.TypeInstance.Type, + entry.Definition, + _domainResolver + ); + } + + // Phase 10: Backwards dependencies + foreach (var entry in typesToProcess) + { + TypeProcessor.AddBackwardsDependencies(entry.TypeInstance.Type); + } + + // Phase 11: Register types with their namespaces + TypeProcessor.AddTypesToNamespaces( + _typesToProcess.Values.Select(entry => entry.TypeInstance.Type) + ); + } } } diff --git a/ArchUnitNET/Loader/ArchLoader.cs b/ArchUnitNET/Loader/ArchLoader.cs index 5ac7ab920..be28272df 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,279 @@ namespace ArchUnitNET.Loader { public class ArchLoader { - private readonly ArchBuilder _archBuilder = new ArchBuilder(); - private DotNetCoreAssemblyResolver _assemblyResolver = new DotNetCoreAssemblyResolver(); + private bool _skipRuleEvaluationCache; + private bool _skipArchitectureCache; + private readonly List _loadInstructions = new List(); - public Architecture Build() + // --------------------------------------------------------------------------- + // LoadInstruction hierarchy — replaces the old tagged-union struct + // --------------------------------------------------------------------------- + + 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 - ) + // --------------------------------------------------------------------------- + // 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() { - var assemblySet = new HashSet(assemblies); - assemblySet.ForEach(assembly => LoadAssemblyIncludingDependencies(assembly, recursive)); + _skipRuleEvaluationCache = true; return this; } - public ArchLoader LoadFilteredDirectory( - string directory, - string filter, - SearchOption searchOption = TopDirectoryOnly - ) + /// + /// 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() { - var path = Path.GetFullPath(directory); - _assemblyResolver.AssemblyPath = path; - var assemblies = Directory.GetFiles(path, filter, searchOption); + _skipArchitectureCache = true; + return this; + } - var result = this; - return assemblies.Aggregate( - result, - (current, assembly) => current.LoadAssembly(assembly, false, false) - ); + // --------------------------------------------------------------------------- + // Build + // --------------------------------------------------------------------------- + + public Architecture Build() + { + 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(_skipRuleEvaluationCache, _skipArchitectureCache); + } + finally + { + assemblyResolver.Dispose(); + } } - public ArchLoader LoadFilteredDirectoryIncludingDependencies( - string directory, - string filter, - bool recursive = false, - 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 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 + ); + } + } - var result = this; - return assemblies.Aggregate( - result, - (current, assembly) => current.LoadAssembly(assembly, true, recursive) + private static void ProcessInstruction( + DotNetCoreAssemblyResolver assemblyResolver, + ArchBuilder archBuilder, + FileLoadInstruction instruction + ) + { + 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 +296,19 @@ private void LoadModule( { var module = ModuleDefinition.ReadModule( fileName, - new ReaderParameters { AssemblyResolver = _assemblyResolver } + 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); + assemblyResolver.AddLib(module.Assembly); + archBuilder.AddAssembly(module.Assembly, false); foreach (var assemblyReference in module.AssemblyReferences) { if (includeDependencies && recursive) { AddReferencedAssembliesRecursively( + assemblyResolver, + archBuilder, assemblyReference, processedAssemblies, resolvedModules, @@ -144,14 +319,14 @@ private void LoadModule( { try { - processedAssemblies.Add(assemblyReference); - _assemblyResolver.AddLib(assemblyReference); + processedAssemblies.Add(assemblyReference.FullName); + 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 +337,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,30 +349,32 @@ private void LoadModule( } } - private void AddReferencedAssembliesRecursively( + private static void AddReferencedAssembliesRecursively( + DotNetCoreAssemblyResolver assemblyResolver, + ArchBuilder archBuilder, 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); + 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 +386,8 @@ var reference in assemblyDefinition.Modules.SelectMany(m => { if (filterResult?.TraverseDependencies != false) AddReferencedAssembliesRecursively( + assemblyResolver, + archBuilder, reference, processedAssemblies, resolvedModules, @@ -221,23 +400,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; - } } } 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/TypeFactory.cs b/ArchUnitNET/Loader/DomainResolver.cs similarity index 75% rename from ArchUnitNET/Loader/TypeFactory.cs rename to ArchUnitNET/Loader/DomainResolver.cs index 240598597..287d747ae 100644 --- a/ArchUnitNET/Loader/TypeFactory.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,11 +12,20 @@ namespace ArchUnitNET.Loader { - internal class TypeFactory + /// + /// 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 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>(); @@ -25,31 +33,83 @@ internal class TypeFactory private readonly Dictionary _allMethods = new Dictionary(); - public TypeFactory( - LoadTaskRegistry loadTaskRegistry, - AssemblyRegistry assemblyRegistry, - NamespaceRegistry namespaceRegistry + private readonly Dictionary _allFields = + new Dictionary(); + + /// + /// 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, + bool isOnlyReferenced, + List assemblyReferences ) { - _loadTaskRegistry = loadTaskRegistry; - _assemblyRegistry = assemblyRegistry; - _namespaceRegistry = namespaceRegistry; + if (_assemblies.TryGetValue(assemblyFullName, out var existing)) + { + return existing; + } + + var assembly = new Assembly( + assemblyName, + assemblyFullName, + isOnlyReferenced, + assemblyReferences + ); + _assemblies.Add(assemblyFullName, assembly); + return assembly; } - public IEnumerable GetAllNonCompilerGeneratedTypes() + /// + /// Returns the existing for the given name, or creates + /// and caches a new one. + /// + internal Namespace GetOrCreateNamespace(string namespaceName) { - return _allTypes - .Values.Select(instance => instance.Type) - .Distinct() - .Where(type => !type.IsCompilerGenerated); + if (_namespaces.TryGetValue(namespaceName, out var existing)) + { + return existing; + } + + var ns = new Namespace(namespaceName, new List()); + _namespaces.Add(namespaceName, ns); + return ns; } + /// + /// 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 @@ -96,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) { @@ -121,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( @@ -263,10 +318,8 @@ bool isStub { declaringTypeReference = declaringTypeReference.DeclaringType; } - var currentNamespace = _namespaceRegistry.GetOrCreateNamespace( - declaringTypeReference.Namespace - ); - var currentAssembly = _assemblyRegistry.GetOrCreateAssembly( + var currentNamespace = GetOrCreateNamespace(declaringTypeReference.Namespace); + var currentAssembly = GetOrCreateAssembly( assemblyFullName, assemblyFullName, true, @@ -323,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); @@ -346,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)) { @@ -358,15 +402,15 @@ TypeReference typeReference var result = new TypeInstance( new UnavailableType( new Type( - typeReference.BuildFullName(), + typeReferenceFullName, 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, @@ -432,7 +476,7 @@ FunctionPointerType functionPointerType Public, false, false, - false, + true, false ); var returnTypeInstance = GetOrCreateStubTypeInstanceFromTypeReference( @@ -450,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, @@ -459,15 +507,22 @@ [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 genericResult = CreateGenericInstanceMethodMemberFromMethodReference( + typeInstance, + methodReference ); + _allMethods.Add(methodReferenceFullName, genericResult); + return genericResult; } if (_allMethods.TryGetValue(methodReferenceFullName, out var existingMethodInstance)) @@ -476,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; @@ -572,36 +627,102 @@ 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 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."); + } + + /// + /// Extracts generic parameters from a Cecil generic parameter provider (type or method) + /// and returns them as domain instances. + /// public IEnumerable GetGenericParameters( IGenericParameterProvider genericParameterProvider ) @@ -615,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/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/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 deleted file mode 100644 index 83b73cf22..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddAttributesAndAttributeDependencies.cs +++ /dev/null @@ -1,230 +0,0 @@ -// 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; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; -using JetBrains.Annotations; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - internal class AddAttributesAndAttributeDependencies : ILoadTask - { - private readonly IType _type; - private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; - - public AddAttributesAndAttributeDependencies( - IType type, - TypeDefinition typeDefinition, - TypeFactory typeFactory - ) - { - _type = type; - _typeDefinition = typeDefinition; - _typeFactory = typeFactory; - } - - public void Execute() - { - _typeDefinition.CustomAttributes.ForEach( - AddAttributeArgumentReferenceDependenciesToOriginType - ); - var typeAttributeInstances = CreateAttributesFromCustomAttributes( - _typeDefinition.CustomAttributes - ) - .ToList(); - _type.AttributeInstances.AddRange(typeAttributeInstances); - var typeAttributeDependencies = typeAttributeInstances.Select( - attributeInstance => new AttributeTypeDependency(_type, attributeInstance) - ); - _type.Dependencies.AddRange(typeAttributeDependencies); - SetUpAttributesForTypeGenericParameters(); - CollectAttributesForMembers(); - } - - private void SetUpAttributesForTypeGenericParameters() - { - foreach (var genericParameter in _typeDefinition.GenericParameters) - { - var param = _type.GenericParameters.First(parameter => - parameter.Name == genericParameter.Name - ); - var attributeInstances = CreateAttributesFromCustomAttributes( - genericParameter.CustomAttributes - ) - .ToList(); - _type.AttributeInstances.AddRange(attributeInstances); - param.AttributeInstances.AddRange(attributeInstances); - var genericParameterAttributeDependencies = attributeInstances.Select( - attributeInstance => new AttributeTypeDependency(_type, attributeInstance) - ); - _type.Dependencies.AddRange(genericParameterAttributeDependencies); - } - } - - private void CollectAttributesForMembers() - { - _typeDefinition - .Fields.Where(x => !x.IsBackingField() && !x.IsCompilerGenerated()) - .ForEach(SetUpAttributesForFields); - - _typeDefinition - .Properties.Where(x => !x.IsCompilerGenerated()) - .ForEach(SetUpAttributesForProperties); - - _typeDefinition - .Methods.Where(x => !x.IsCompilerGenerated()) - .ForEach(SetUpAttributesForMethods); - } - - private void SetUpAttributesForFields(FieldDefinition fieldDefinition) - { - var fieldMember = _type - .GetFieldMembers() - .WhereFullNameIs(fieldDefinition.FullName) - .RequiredNotNull(); - CollectMemberAttributesAndDependencies( - fieldMember, - fieldDefinition.CustomAttributes.ToList(), - fieldMember.MemberDependencies - ); - } - - private void SetUpAttributesForProperties(PropertyDefinition propertyDefinition) - { - var propertyMember = _type - .GetPropertyMembers() - .WhereFullNameIs(propertyDefinition.FullName) - .RequiredNotNull(); - CollectMemberAttributesAndDependencies( - propertyMember, - propertyDefinition.CustomAttributes.ToList(), - propertyMember.AttributeDependencies - ); - } - - private void SetUpAttributesForMethods(MethodDefinition methodDefinition) - { - var methodMember = _type - .GetMethodMembers() - .WhereFullNameIs(methodDefinition.BuildFullName()) - .RequiredNotNull(); - var memberCustomAttributes = methodDefinition.GetAllMethodCustomAttributes().ToList(); - SetUpAttributesForMethodGenericParameters(methodDefinition, methodMember); - CollectMemberAttributesAndDependencies( - methodMember, - memberCustomAttributes, - methodMember.MemberDependencies - ); - } - - private void SetUpAttributesForMethodGenericParameters( - MethodDefinition methodDefinition, - MethodMember methodMember - ) - { - foreach (var genericParameter in methodDefinition.GenericParameters) - { - var param = methodMember.GenericParameters.First(parameter => - parameter.Name == genericParameter.Name - ); - var customAttributes = genericParameter.CustomAttributes; - customAttributes.ForEach(AddAttributeArgumentReferenceDependenciesToOriginType); - var attributeInstances = CreateAttributesFromCustomAttributes(customAttributes) - .ToList(); - methodMember.AttributeInstances.AddRange(attributeInstances); - param.AttributeInstances.AddRange(attributeInstances); - var genericParameterAttributeDependencies = attributeInstances.Select( - attributeInstance => new AttributeMemberDependency( - methodMember, - attributeInstance - ) - ); - methodMember.MemberDependencies.AddRange(genericParameterAttributeDependencies); - } - } - - private void CollectMemberAttributesAndDependencies( - IMember methodMember, - List memberCustomAttributes, - List attributeDependencies - ) - { - memberCustomAttributes.ForEach(AddAttributeArgumentReferenceDependenciesToOriginType); - var memberAttributeInstances = CreateAttributesFromCustomAttributes( - memberCustomAttributes - ) - .ToList(); - methodMember.AttributeInstances.AddRange(memberAttributeInstances); - var methodAttributeDependencies = CreateMemberAttributeDependencies( - methodMember, - memberAttributeInstances - ); - attributeDependencies.AddRange(methodAttributeDependencies); - } - - [NotNull] - public IEnumerable CreateAttributesFromCustomAttributes( - IEnumerable customAttributes - ) - { - 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(_typeFactory)); - } - - [NotNull] - private static IEnumerable CreateMemberAttributeDependencies( - IMember member, - IEnumerable attributes - ) - { - return attributes.Select(attributeInstance => new AttributeMemberDependency( - member, - attributeInstance - )); - } - - private void AddAttributeArgumentReferenceDependenciesToOriginType( - ICustomAttribute customAttribute - ) - { - 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 = _typeFactory.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 a50b51fc3..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddBackwardsDependencies.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; - -namespace ArchUnitNET.Loader.LoadTasks -{ - internal class AddBackwardsDependencies : ILoadTask - { - private readonly IType _type; - - public AddBackwardsDependencies(IType type) - { - _type = type; - } - - public void Execute() - { - _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 9b8a3e299..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddBaseClassDependency.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - internal class AddBaseClassDependency : ILoadTask - { - private readonly IType _cls; - private readonly Type _type; - private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; - - public AddBaseClassDependency( - IType cls, - Type type, - TypeDefinition typeDefinition, - TypeFactory typeFactory - ) - { - _cls = cls; - _type = type; - _typeDefinition = typeDefinition; - _typeFactory = typeFactory; - } - - public void Execute() - { - var typeDefinitionBaseType = _typeDefinition?.BaseType; - - if (typeDefinitionBaseType == null) - { - return; - } - - var baseType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - typeDefinitionBaseType - ); - if (!(baseType.Type is Class baseClass)) - { - return; - } - - var dependency = new InheritsBaseClassDependency( - _cls, - 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 f34eb2112..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddClassDependencies.cs +++ /dev/null @@ -1,71 +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 -{ - internal class AddClassDependencies : ILoadTask - { - private readonly List _dependencies; - private readonly IType _type; - private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; - - public AddClassDependencies( - IType type, - TypeDefinition typeDefinition, - TypeFactory typeFactory, - List dependencies - ) - { - _type = type; - _typeDefinition = typeDefinition; - _typeFactory = typeFactory; - _dependencies = dependencies; - } - - public void Execute() - { - AddInterfaceDependencies(); - AddMemberDependencies(); - } - - private void AddMemberDependencies() - { - _type.Members.ForEach(member => - { - _dependencies.AddRange(member.Dependencies); - }); - } - - private void AddInterfaceDependencies() - { - GetInterfacesImplementedByClass(_typeDefinition) - .ForEach(target => - { - var targetType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - target - ); - _dependencies.Add(new ImplementsInterfaceDependency(_type, targetType)); - }); - } - - 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 041c072ba..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddFieldAndPropertyDependencies.cs +++ /dev/null @@ -1,43 +0,0 @@ -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using ArchUnitNET.Domain.Extensions; - -namespace ArchUnitNET.Loader.LoadTasks -{ - internal class AddFieldAndPropertyDependencies : ILoadTask - { - private readonly IType _type; - - public AddFieldAndPropertyDependencies(IType type) - { - _type = type; - } - - public void Execute() - { - _type - .GetFieldMembers() - .ForEach(field => - { - var dependency = new FieldTypeDependency(field); - AddDependencyIfMissing(field, dependency); - }); - - _type - .GetPropertyMembers() - .ForEach(property => - { - var dependency = new PropertyTypeDependency(property); - AddDependencyIfMissing(property, 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 deleted file mode 100644 index c109be34b..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddGenericArgumentDependencies.cs +++ /dev/null @@ -1,114 +0,0 @@ -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 - { - [NotNull] - private readonly IType _type; - - public AddGenericArgumentDependencies([NotNull] IType type) - { - _type = type; - } - - public void Execute() - { - AddTypeGenericArgumentDependencies(); - AddMemberGenericArgumentDependencies(); - - var typeDependencies = new List(); - foreach (var dependency in _type.Dependencies) - { - FindGenericArgumentsInTypeDependenciesRecursive( - dependency.TargetGenericArguments, - typeDependencies - ); - } - - _type.Dependencies.AddRange(typeDependencies); - - 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 void AddTypeGenericArgumentDependencies() - { - foreach (var parameter in _type.GenericParameters) - { - _type.Dependencies.AddRange(parameter.Dependencies); - } - } - - private void FindGenericArgumentsInTypeDependenciesRecursive( - IEnumerable targetGenericArguments, - ICollection createdDependencies - ) - { - foreach ( - var genericArgument in targetGenericArguments.Where(argument => - !argument.Type.IsGenericParameter - ) - ) - { - createdDependencies.Add(new GenericArgumentTypeDependency(_type, genericArgument)); - FindGenericArgumentsInTypeDependenciesRecursive( - 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, - 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 fc9ed1ae4..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddGenericParameterDependencies.cs +++ /dev/null @@ -1,58 +0,0 @@ -using ArchUnitNET.Domain; -using ArchUnitNET.Domain.Dependencies; -using JetBrains.Annotations; - -namespace ArchUnitNET.Loader.LoadTasks -{ - public class AddGenericParameterDependencies : ILoadTask - { - [NotNull] - private readonly IType _type; - - public AddGenericParameterDependencies([NotNull] IType type) - { - _type = type; - } - - public void Execute() - { - AddTypeGenericParameterDependencies(); - AddMemberGenericParameterDependencies(); - } - - private void AddTypeGenericParameterDependencies() - { - 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); - } - } - } - - private void AddMemberGenericParameterDependencies() - { - 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 bbb0d86fe..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddMembers.cs +++ /dev/null @@ -1,173 +0,0 @@ -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 -{ - internal class AddMembers : ILoadTask - { - private readonly MemberList _memberList; - private readonly ITypeInstance _typeInstance; - private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; - - public AddMembers( - ITypeInstance typeInstance, - TypeDefinition typeDefinition, - TypeFactory typeFactory, - MemberList memberList - ) - { - _typeInstance = typeInstance; - _typeDefinition = typeDefinition; - _typeFactory = typeFactory; - _memberList = memberList; - } - - public void Execute() - { - var members = CreateMembers(_typeDefinition); - _memberList.AddRange(members); - } - - [NotNull] - private IEnumerable CreateMembers([NotNull] TypeDefinition typeDefinition) - { - return typeDefinition - .Fields.Where(fieldDefinition => !fieldDefinition.IsBackingField()) - .Select(CreateFieldMember) - .Concat( - typeDefinition - .Properties.Select(CreatePropertyMember) - .Concat( - typeDefinition.Methods.Select(method => - _typeFactory - .GetOrCreateMethodMemberFromMethodReference( - _typeInstance, - method - ) - .Member - ) - ) - ) - .Where(member => !member.IsCompilerGenerated); - } - - [NotNull] - private IMember CreateFieldMember([NotNull] FieldDefinition fieldDefinition) - { - var typeReference = fieldDefinition.FieldType; - var fieldType = _typeFactory.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) - { - var typeReference = propertyDefinition.PropertyType; - var propertyType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - typeReference - ); - var isCompilerGenerated = propertyDefinition.IsCompilerGenerated(); - var isStatic = - (propertyDefinition.SetMethod != null && propertyDefinition.SetMethod.IsStatic) - || (propertyDefinition.GetMethod != null && propertyDefinition.GetMethod.IsStatic); - var writeAccessor = GetWriteAccessor(propertyDefinition); - return new PropertyMember( - _typeInstance.Type, - propertyDefinition.Name, - propertyDefinition.FullName, - propertyType, - isCompilerGenerated, - isStatic, - writeAccessor - ); - } - - 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; - - if (isReadOnly) - { - return Writability.ReadOnly; - } - - bool isInitSetter = CheckPropertyHasInitSetterInNetStandardCompatibleWay( - propertyDefinition - ); - return isInitSetter ? Writability.InitOnly : Writability.Writable; - } - - private static bool CheckPropertyHasInitSetterInNetStandardCompatibleWay( - PropertyDefinition propertyDefinition - ) - { - return propertyDefinition.SetMethod?.ReturnType.IsRequiredModifier == true - && ((RequiredModifierType)propertyDefinition.SetMethod.ReturnType) - .ModifierType - .FullName == "System.Runtime.CompilerServices.IsExternalInit"; - } - } -} diff --git a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs b/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs deleted file mode 100644 index 5878c6d28..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddMethodDependencies.cs +++ /dev/null @@ -1,477 +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 -{ - internal class AddMethodDependencies : ILoadTask - { - private readonly IType _type; - private readonly TypeDefinition _typeDefinition; - private readonly TypeFactory _typeFactory; - - public AddMethodDependencies( - IType type, - TypeDefinition typeDefinition, - TypeFactory typeFactory - ) - { - _type = type; - _typeDefinition = typeDefinition; - _typeFactory = typeFactory; - } - - 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 => - { - var (methodMember, methodDefinition) = tuple; - var dependencies = CreateMethodSignatureDependencies( - methodDefinition, - methodMember - ) - .Concat(CreateMethodBodyDependencies(methodDefinition, methodMember)); - if (methodDefinition.IsSetter || methodDefinition.IsGetter) - { - AssignDependenciesToProperty(methodMember, methodDefinition); - } - - return (methodMember, dependencies); - }) - .ForEach(tuple => - { - var (methodMember, dependencies) = tuple; - methodMember.MemberDependencies.AddRange(dependencies); - }); - } - - private void AssignDependenciesToProperty( - MethodMember methodMember, - MethodDefinition methodDefinition - ) - { - var methodForm = methodDefinition.GetMethodForm(); - var matchFunction = GetMatchFunction(methodForm); - matchFunction.RequiredNotNull(); - - var accessedProperty = MatchToPropertyMember( - methodMember.Name, - methodMember.FullName, - matchFunction - ); - if (accessedProperty == null) - { - return; - } - - accessedProperty.IsVirtual = accessedProperty.IsVirtual || methodMember.IsVirtual; - - 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 IEnumerable CreateMethodSignatureDependencies( - MethodReference methodReference, - MethodMember methodMember - ) - { - var returnType = methodReference.GetReturnType(_typeFactory); - return (returnType != null ? new[] { returnType } : Array.Empty>()) - .Concat(methodReference.GetParameters(_typeFactory)) - .Concat(methodReference.GetGenericParameters(_typeFactory)) - .Distinct() - .Select(signatureType => new MethodSignatureDependency( - methodMember, - signatureType - )); - } - - [NotNull] - private IEnumerable CreateMethodBodyDependencies( - MethodDefinition methodDefinition, - MethodMember methodMember - ) - { - 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 - ); - } - - if (methodDefinition.IsIterator()) - { - HandleIterator( - out methodDefinition, - ref methodBody, - bodyTypes, - visitedMethodReferences - ); - } - - bodyTypes.AddRange(methodDefinition.GetBodyTypes(_typeFactory).ToList()); - - var castTypes = methodDefinition.GetCastTypes(_typeFactory).ToList(); - - var typeCheckTypes = methodDefinition.GetTypeCheckTypes(_typeFactory).ToList(); - - var metaDataTypes = methodDefinition.GetMetaDataTypes(_typeFactory).ToList(); - - var accessedFieldMembers = methodDefinition - .GetAccessedFieldMembers(_typeFactory) - .ToList(); - - var calledMethodMembers = CreateMethodBodyDependenciesRecursive( - methodBody, - visitedMethodReferences, - bodyTypes, - castTypes, - typeCheckTypes, - metaDataTypes, - accessedFieldMembers - ); - - 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 IEnumerable CreateMethodBodyDependenciesRecursive( - MethodBody methodBody, - ICollection visitedMethodReferences, - List> bodyTypes, - List> castTypes, - List> typeCheckTypes, - List> metaDataTypes, - List accessedFieldMembers - ) - { - var calledMethodReferences = methodBody - .Instructions.Select(instruction => instruction.Operand) - .OfType(); - - foreach ( - var calledMethodReference in calledMethodReferences.Except(visitedMethodReferences) - ) - { - visitedMethodReferences.Add(calledMethodReference); - - var calledType = _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - calledMethodReference.DeclaringType - ); - var calledMethodMember = _typeFactory.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 - ); - } - - bodyTypes.AddRange(calledMethodDefinition.GetBodyTypes(_typeFactory)); - castTypes.AddRange(calledMethodDefinition.GetCastTypes(_typeFactory)); - typeCheckTypes.AddRange(calledMethodDefinition.GetTypeCheckTypes(_typeFactory)); - metaDataTypes.AddRange(calledMethodDefinition.GetMetaDataTypes(_typeFactory)); - accessedFieldMembers.AddRange( - calledMethodDefinition.GetAccessedFieldMembers(_typeFactory) - ); - - foreach ( - var dep in CreateMethodBodyDependenciesRecursive( - calledMethodBody, - visitedMethodReferences, - bodyTypes, - castTypes, - typeCheckTypes, - metaDataTypes, - accessedFieldMembers - ) - ) - { - yield return dep; - } - } - else - { - yield return calledMethodMember; - } - } - } - - private void HandleIterator( - out MethodDefinition methodDefinition, - ref MethodBody methodBody, - List> bodyTypes, - ICollection visitedMethodReferences - ) - { - 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 => - _typeFactory.GetOrCreateStubTypeInstanceFromTypeReference(bodyField.FieldType) - ) - ); - } - - private void HandleAsync( - out MethodDefinition methodDefinition, - ref MethodBody methodBody, - List> bodyTypes, - ICollection visitedMethodReferences - ) - { - 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 => - _typeFactory.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 deleted file mode 100644 index d934104f5..000000000 --- a/ArchUnitNET/Loader/LoadTasks/AddTypesToNamespaces.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using ArchUnitNET.Domain; - -namespace ArchUnitNET.Loader.LoadTasks -{ - internal class AddTypesToNamespaces : ILoadTask - { - private readonly List _types; - - public AddTypesToNamespaces(List types) - { - _types = types; - } - - public void Execute() - { - 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 c9ffd2bc5..000000000 --- a/ArchUnitNET/Loader/LoadTasks/CollectAssemblyAttributes.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Linq; -using ArchUnitNET.Domain; -using Mono.Cecil; - -namespace ArchUnitNET.Loader.LoadTasks -{ - internal class CollectAssemblyAttributes : ILoadTask - { - private readonly Assembly _assembly; - private readonly AssemblyDefinition _assemblyDefinition; - private readonly TypeFactory _typeFactory; - - public CollectAssemblyAttributes( - Assembly assembly, - AssemblyDefinition assemblyDefinition, - TypeFactory typeFactory - ) - { - _assembly = assembly; - _assemblyDefinition = assemblyDefinition; - _typeFactory = typeFactory; - } - - public void Execute() - { - var attributeInstances = _assemblyDefinition - .CustomAttributes.Select(attr => - attr.CreateAttributeFromCustomAttribute(_typeFactory) - ) - .ToList(); - _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/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 20379af43..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; @@ -30,11 +29,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] @@ -84,120 +90,187 @@ 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 ); + /// + /// 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, - TypeFactory typeFactory + 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 => - typeFactory.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 typeFactory.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, - TypeFactory typeFactory - ) + /// + /// 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 => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - (TypeReference)inst.Operand - ) - ); - } + internal static readonly FieldReferenceNameComparer Instance = + new FieldReferenceNameComparer(); - [NotNull] - internal static IEnumerable> GetMetaDataTypes( - this MethodDefinition methodDefinition, - TypeFactory typeFactory - ) - { - var instructions = - methodDefinition.Body?.Instructions ?? Enumerable.Empty(); - - return instructions - .Where(inst => inst.OpCode == OpCodes.Ldtoken && inst.Operand is TypeReference) - .Select(inst => - typeFactory.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, - TypeFactory typeFactory - ) - { - var instructions = - methodDefinition.Body?.Instructions ?? Enumerable.Empty(); - - return instructions - .Where(inst => inst.OpCode == OpCodes.Isinst && inst.Operand is TypeReference) - .Select(inst => - typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - (TypeReference)inst.Operand - ) - ); + public int GetHashCode(FieldReference obj) + { + return obj?.FullName?.GetHashCode() ?? 0; + } } internal static bool IsIterator(this MethodDefinition methodDefinition) @@ -216,52 +289,6 @@ internal static bool IsAsync(this MethodDefinition methodDefinition) ); } - [NotNull] - internal static IEnumerable GetAccessedFieldMembers( - this MethodDefinition methodDefinition, - TypeFactory typeFactory - ) - { - 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 = typeFactory.GetOrCreateStubTypeInstanceFromTypeReference( - fieldReference.DeclaringType - ); - var matchingFieldMembers = declaringType - .Type.GetFieldMembers() - .Where(member => member.Name == fieldReference.Name) - .ToList(); - - switch (matchingFieldMembers.Count) - { - case 0: - var stubFieldMember = typeFactory.CreateStubFieldMemberFromFieldReference( - 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()) 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/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/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; - } - } -} 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; } + } +} 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/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/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/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; 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..8d75c21b3 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 both rule evaluation and architecture caching +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