Repository navigation
feat: let member rules include the members of referenced types - #518
Open
StefanGreve wants to merge 1 commit into
Open
StefanGreve wants to merge 1 commit into
StefanGreve wants to merge 1 commit into
Conversation
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 TNG#198
Signed-off-by: StefanGreve <stefan.ohlsen.greve@gmail.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #518 +/- ##
==========================================
+ Coverage 86.55% 86.61% +0.06%
==========================================
Files 261 261
Lines 12648 12697 +49
Branches 1236 1241 +5
==========================================
+ Hits 10947 10998 +51
+ Misses 1359 1357 -2
Partials 342 342 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Member rules can now include the members of referenced types, so that a rule can forbid or require a call to a framework member, which #198 asks for with
DateTime.Now:On 0.13.5 the same rule without
truepasses on code that readsDateTime.Now. Its object provider resolves to an empty set, so the condition holds for every type.Why a provider alone is not enough
Mirroring
Types(bool includeReferenced)for members does not fix this on its own.DomainResolver.GetOrCreateMethodMemberFromMethodReferencecreates the member a call site targets, with the referenced type as itsDeclaringType, and caches it in_allMethods. It never adds the member to that type'sMembers, because referenced types never pass throughTypeProcessor.AddMembers. A referencedSystem.Consoleis therefore the declaring type of the call target and still reports zero members, so a provider overTypes.Concat(ReferencedTypes)would find nothing either.Changes
ArchBuilder.ProcessTypesadds every method and field member the resolver created for a referenced type to that type's members. It runs after method body dependencies, which create these members. Generic parameters and compiler-generated members are skipped, as elsewhere in the loader.Architecture.ReferencedMembers,ReferencedFieldMembersandReferencedMethodMembers, besideReferencedClassesand its siblings.Members(bool includeReferenced = false),FieldMembers(...)andMethodMembers(...), backed byMembersIncludingReferenced,FieldMembersIncludingReferencedandMethodMembersIncludingReferencedinBasicObjectProviderDefinition. The descriptions read "Method members (including referenced)" and so on, likeTypes(true).PropertyMembers()is unchanged. IL references accessor methods such asget_Now()and never the property, so a referenced type never gets a property member andPropertyMembers(true)would always be empty. The fluent API already reaches the accessor throughMethodMembers(true), as shown above.Behavior changes worth checking
ReferencedTypesand assumes emptyMemberswill see them. One consequence is thatClass.MembersIncludingInheritedon a loaded class whose base class is referenced now also contains the base members the loaded code uses. No existing test or snapshot changed.Members(),FieldMembers()andMethodMembers(). Source stays compatible, but code compiled against the old signatures needs a rebuild.Types()and the other type entry points took the same step when they gainedincludeReferenced.Tests
Three tests in
DependenciesToOtherAssembliesTests, whose architecture loads onlyArchUnitNETTests, so thatTestAssemblytypes and framework types are referenced:IncludeReferencedMembersTest: a called method and a read field of the referencedClassInTestAssemblyare returned byMethodMembers(true),FieldMembers(true)andMembers(true), and not by the variants without the flag.ReferencedTypeHasOnlyTheMembersLoadedTypesUseTest: an unused method of that class is not registered, and every referenced member belongs to a stub type.NotCallAnyReferencedMethodTest: the rule from How to check for system members? #198, withCallAnypassing andNotCallAnyfailing on a class that readsDateTime.Now.With the new loader phase commented out, the first and the third test fail.
dotnet testpasses on both solution filters, anddotnet csharpier check .reports no changes.Open question: empty argument providers
Should a negative condition fail when its object provider resolves to nothing? This PR fixes the specific case, but the general one remains:
Types().Should().NotCallAny(MethodMembers().That().AreDeclaredIn(typeof(DateTime)))still passes on code that readsDateTime.Now, because withouttruethe provider is empty. The rule reads as a guarantee and checks nothing, and nothing reports it.The subject side already guards against this: a rule whose subjects resolve to an empty set fails unless
WithoutRequiringPositiveResults()says otherwise. The argument side ofNotCallAny,NotDependOnAnyand their siblings has no such guard, which is where #198's case went unnoticed. Would you accept a follow-up that makes an empty argument provider fail the rule by default, with the same opt-out? It would break existing rules whose argument provider is legitimately empty, so I would rather ask before writing it.Resolves #198