diff --git a/ArchUnitNET/Domain/Architecture.cs b/ArchUnitNET/Domain/Architecture.cs index bbeb130ed..886ccafa2 100644 --- a/ArchUnitNET/Domain/Architecture.cs +++ b/ArchUnitNET/Domain/Architecture.cs @@ -68,6 +68,17 @@ public Architecture( public IEnumerable FieldMembers => Members.OfType(); public IEnumerable MethodMembers => Members.OfType(); public IEnumerable Members => Types.SelectMany(type => type.Members); + public IEnumerable ReferencedFieldMembers => + ReferencedMembers.OfType(); + public IEnumerable ReferencedMethodMembers => + ReferencedMembers.OfType(); + + /// + /// The members of referenced types that the loaded types use, such as a framework method + /// they call or a framework field they read. A referenced type carries no other members. + /// + public IEnumerable ReferencedMembers => + ReferencedTypes.SelectMany(type => type.Members); /// /// Returns the cached result for the given object provider, or invokes the providing diff --git a/ArchUnitNET/Fluent/ArchRuleDefinition.cs b/ArchUnitNET/Fluent/ArchRuleDefinition.cs index 943dcbbe2..ac0046cc3 100644 --- a/ArchUnitNET/Fluent/ArchRuleDefinition.cs +++ b/ArchUnitNET/Fluent/ArchRuleDefinition.cs @@ -50,25 +50,33 @@ public static GivenInterfaces Interfaces(bool includeReferenced = false) return new GivenInterfaces(ruleCreator); } - public static GivenMembers Members() + public static GivenMembers Members(bool includeReferenced = false) { - var ruleCreator = new ArchRuleCreator(BasicObjectProviderDefinition.Members); + var ruleCreator = includeReferenced + ? new ArchRuleCreator( + BasicObjectProviderDefinition.MembersIncludingReferenced + ) + : new ArchRuleCreator(BasicObjectProviderDefinition.Members); return new GivenMembers(ruleCreator); } - public static GivenFieldMembers FieldMembers() + public static GivenFieldMembers FieldMembers(bool includeReferenced = false) { - var ruleCreator = new ArchRuleCreator( - BasicObjectProviderDefinition.FieldMembers - ); + var ruleCreator = includeReferenced + ? new ArchRuleCreator( + BasicObjectProviderDefinition.FieldMembersIncludingReferenced + ) + : new ArchRuleCreator(BasicObjectProviderDefinition.FieldMembers); return new GivenFieldMembers(ruleCreator); } - public static GivenMethodMembers MethodMembers() + public static GivenMethodMembers MethodMembers(bool includeReferenced = false) { - var ruleCreator = new ArchRuleCreator( - BasicObjectProviderDefinition.MethodMembers - ); + var ruleCreator = includeReferenced + ? new ArchRuleCreator( + BasicObjectProviderDefinition.MethodMembersIncludingReferenced + ) + : new ArchRuleCreator(BasicObjectProviderDefinition.MethodMembers); return new GivenMethodMembers(ruleCreator); } diff --git a/ArchUnitNET/Fluent/BasicObjectProviderDefinition.cs b/ArchUnitNET/Fluent/BasicObjectProviderDefinition.cs index 727a22907..27e7c58a4 100644 --- a/ArchUnitNET/Fluent/BasicObjectProviderDefinition.cs +++ b/ArchUnitNET/Fluent/BasicObjectProviderDefinition.cs @@ -71,5 +71,25 @@ public static class BasicObjectProviderDefinition architecture => architecture.Interfaces.Concat(architecture.ReferencedInterfaces), "Interfaces (including referenced)" ); + + public static readonly BasicObjectProvider MembersIncludingReferenced = + new BasicObjectProvider( + architecture => architecture.Members.Concat(architecture.ReferencedMembers), + "Members (including referenced)" + ); + + public static readonly BasicObjectProvider FieldMembersIncludingReferenced = + new BasicObjectProvider( + architecture => + architecture.FieldMembers.Concat(architecture.ReferencedFieldMembers), + "Field members (including referenced)" + ); + + public static readonly BasicObjectProvider MethodMembersIncludingReferenced = + new BasicObjectProvider( + architecture => + architecture.MethodMembers.Concat(architecture.ReferencedMethodMembers), + "Method members (including referenced)" + ); } } diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs index 9df3c9df8..601ab8de3 100644 --- a/ArchUnitNET/Loader/ArchBuilder.cs +++ b/ArchUnitNET/Loader/ArchBuilder.cs @@ -305,6 +305,13 @@ from entry in typesToProcess TypeProcessor.AddTypesToNamespaces( _typesToProcess.Values.Select(entry => entry.TypeInstance.Type) ); + + // Phase 12: Register referenced members with their referenced types + TypeProcessor.AddReferencedMembers( + _domainResolver + .MethodMemberInstances.Select(instance => (IMember)instance.Member) + .Concat(_domainResolver.FieldMembers) + ); } } } diff --git a/ArchUnitNET/Loader/DomainResolver.cs b/ArchUnitNET/Loader/DomainResolver.cs index 5872974d6..a33e3cfaa 100644 --- a/ArchUnitNET/Loader/DomainResolver.cs +++ b/ArchUnitNET/Loader/DomainResolver.cs @@ -51,6 +51,16 @@ internal class DomainResolver /// public IEnumerable> Types => _allTypes.Values; + /// + /// All method members that have been created or cached, including generic method instances. + /// + internal IEnumerable MethodMemberInstances => _allMethods.Values; + + /// + /// All field members that have been created or cached. + /// + internal IEnumerable FieldMembers => _allFields.Values; + /// /// Returns the existing for the given full name, or creates /// and caches a new one. diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs index dceed9363..b96c0da44 100644 --- a/ArchUnitNET/Loader/TypeProcessor.cs +++ b/ArchUnitNET/Loader/TypeProcessor.cs @@ -1122,6 +1122,30 @@ internal static void AddTypesToNamespaces(IEnumerable types) ((List)type.Namespace.Types).Add(type); } } + + // ── Phase 12: Register referenced members with referenced types ───── + + /// + /// Adds each member the loaded types reference on a referenced type to that type's members. + /// Referenced types never pass through , so without this step a + /// member such as a called framework method is reachable from its call site but not from + /// its declaring type. Must run after method body dependencies, which create these members. + /// + internal static void AddReferencedMembers(IEnumerable members) + { + foreach (var member in members.Distinct()) + { + var declaringType = member.DeclaringType; + if ( + declaringType.IsStub + && !declaringType.IsGenericParameter + && !member.IsCompilerGenerated + ) + { + declaringType.Members.Add(member); + } + } + } } /// diff --git a/ArchUnitNETTests/Fluent/Syntax/DependenciesToOtherAssembliesTests.cs b/ArchUnitNETTests/Fluent/Syntax/DependenciesToOtherAssembliesTests.cs index 287cf8e89..e21dcf9ac 100644 --- a/ArchUnitNETTests/Fluent/Syntax/DependenciesToOtherAssembliesTests.cs +++ b/ArchUnitNETTests/Fluent/Syntax/DependenciesToOtherAssembliesTests.cs @@ -1,4 +1,5 @@ -using ArchUnitNET.Domain; +using System; +using ArchUnitNET.Domain; using ArchUnitNET.Domain.Extensions; using ArchUnitNET.Loader; using ArchUnitNET.xUnit; @@ -128,6 +129,66 @@ public void IncludeReferencedTypesTest() Assert.Contains(interfaceInTestAssembly, Types(true).GetObjects(Architecture)); Assert.Contains(interfaceInTestAssembly, Interfaces(true).GetObjects(Architecture)); } + + [Fact] + public void IncludeReferencedMembersTest() + { + var classInTestAssembly = Architecture.GetClassOfType(typeof(ClassInTestAssembly)); + var method = Assert.Single( + classInTestAssembly.GetMethodMembersWithName("MethodInTestAssembly()") + ); + var field = Assert.Single( + classInTestAssembly.GetFieldMembersWithName("FieldInTestAssembly") + ); + + Assert.Contains(method, MethodMembers(true).GetObjects(Architecture)); + Assert.Contains(method, Members(true).GetObjects(Architecture)); + Assert.Contains(field, FieldMembers(true).GetObjects(Architecture)); + Assert.Contains(field, Members(true).GetObjects(Architecture)); + + Assert.DoesNotContain(method, MethodMembers().GetObjects(Architecture)); + Assert.DoesNotContain(field, FieldMembers().GetObjects(Architecture)); + Assert.DoesNotContain(method, Members().GetObjects(Architecture)); + } + + [Fact] + public void ReferencedTypeHasOnlyTheMembersLoadedTypesUseTest() + { + var classInTestAssembly = Architecture.GetClassOfType(typeof(ClassInTestAssembly)); + + Assert.Empty( + classInTestAssembly.GetMethodMembersWithName("UnusedMethodInTestAssembly()") + ); + Assert.All( + Architecture.ReferencedMembers, + member => Assert.True(member.DeclaringType.IsStub) + ); + } + + [Fact] + public void NotCallAnyReferencedMethodTest() + { + var getNow = MethodMembers(true) + .That() + .AreDeclaredIn(typeof(DateTime)) + .And() + .HaveName("get_Now()"); + + Types() + .That() + .Are(typeof(DependingClass)) + .Should() + .CallAny(getNow) + .Check(Architecture); + Assert.Throws(() => + Types() + .That() + .Are(typeof(DependingClass)) + .Should() + .NotCallAny(getNow) + .Check(Architecture) + ); + } } } @@ -140,6 +201,8 @@ public DependingClass() { dep = new ClassInTestAssembly(); dep.MethodInTestAssembly(); + _ = dep.FieldInTestAssembly; + _ = DateTime.Now; } } } diff --git a/TestAssembly/DependencyTargets/ClassInTestAssembly.cs b/TestAssembly/DependencyTargets/ClassInTestAssembly.cs index dd6bd3195..830bc14ee 100644 --- a/TestAssembly/DependencyTargets/ClassInTestAssembly.cs +++ b/TestAssembly/DependencyTargets/ClassInTestAssembly.cs @@ -2,6 +2,10 @@ { public class ClassInTestAssembly { + public int FieldInTestAssembly; + public void MethodInTestAssembly() { } + + public void UnusedMethodInTestAssembly() { } } }