From f5abea59202bef8b3fbebff586e9d2b1c92139f6 Mon Sep 17 00:00:00 2001 From: StefanGreve Date: Sat, 7 Nov 2026 20:30:54 +0100 Subject: [PATCH] feat: let member rules include the members of referenced types A rule cannot name a member that a loaded type uses on a referenced type, such as a framework method it calls. MethodMembers() reads Architecture.Members, which covers loaded types only, and Types(true) does not help either: the loader creates the member a call site targets and caches it, but never adds it to its declaring type, so a referenced type such as System.Console reports no members at all. A rule like Types().Should().NotCallAny( MethodMembers().That().AreDeclaredIn(typeof(DateTime))) therefore passes on code that reads DateTime.Now, because the object provider resolves to an empty set. Add a loader phase that registers each method and field member created for a referenced type on that type, after method bodies have been processed. A referenced type now carries the members the loaded code uses, and no others. Then give Members(), FieldMembers() and MethodMembers() the includeReferenced parameter that Types(), Classes(), Interfaces() and Attributes() already take, backed by *IncludingReferenced providers and Architecture.ReferencedMembers, ReferencedFieldMembers and ReferencedMethodMembers. The rule above becomes Types().Should().NotCallAny( MethodMembers(true).That().AreDeclaredIn(typeof(DateTime)) .And().HaveName("get_Now()")) PropertyMembers() is left unchanged, because IL references the accessor methods and never the property, so a referenced type has no property members to include. Resolves #198 Signed-off-by: StefanGreve --- ArchUnitNET/Domain/Architecture.cs | 11 ++++ ArchUnitNET/Fluent/ArchRuleDefinition.cs | 28 +++++--- .../Fluent/BasicObjectProviderDefinition.cs | 20 ++++++ ArchUnitNET/Loader/ArchBuilder.cs | 7 ++ ArchUnitNET/Loader/DomainResolver.cs | 10 +++ ArchUnitNET/Loader/TypeProcessor.cs | 24 +++++++ .../DependenciesToOtherAssembliesTests.cs | 65 ++++++++++++++++++- .../DependencyTargets/ClassInTestAssembly.cs | 4 ++ 8 files changed, 158 insertions(+), 11 deletions(-) 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() { } } }