Skip to content

feat: let member rules include the members of referenced types - #518

Open
StefanGreve wants to merge 1 commit into
TNG:mainfrom
StefanGreve:feat/include-referenced-members
Open

StefanGreve wants to merge 1 commit into
TNG:mainfrom
StefanGreve:feat/include-referenced-members

Conversation

@StefanGreve

Copy link
Copy Markdown
Contributor

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:

Types().Should().NotCallAny(
    MethodMembers(true).That().AreDeclaredIn(typeof(DateTime)).And().HaveName("get_Now()"));

On 0.13.5 the same rule without true passes on code that reads DateTime.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.GetOrCreateMethodMemberFromMethodReference creates the member a call site targets, with the referenced type as its DeclaringType, and caches it in _allMethods. It never adds the member to that type's Members, because referenced types never pass through TypeProcessor.AddMembers. A referenced System.Console is therefore the declaring type of the call target and still reports zero members, so a provider over Types.Concat(ReferencedTypes) would find nothing either.

Changes

  • Loader: a new phase 12 in ArchBuilder.ProcessTypes adds 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.
  • Domain: Architecture.ReferencedMembers, ReferencedFieldMembers and ReferencedMethodMembers, beside ReferencedClasses and its siblings.
  • Fluent API: Members(bool includeReferenced = false), FieldMembers(...) and MethodMembers(...), backed by MembersIncludingReferenced, FieldMembersIncludingReferenced and MethodMembersIncludingReferenced in BasicObjectProviderDefinition. The descriptions read "Method members (including referenced)" and so on, like Types(true).

PropertyMembers() is unchanged. IL references accessor methods such as get_Now() and never the property, so a referenced type never gets a property member and PropertyMembers(true) would always be empty. The fluent API already reaches the accessor through MethodMembers(true), as shown above.

Behavior changes worth checking

  • A referenced type now reports the members the loaded code uses instead of none. Code that reads ReferencedTypes and assumes empty Members will see them. One consequence is that Class.MembersIncludingInherited on a loaded class whose base class is referenced now also contains the base members the loaded code uses. No existing test or snapshot changed.
  • Adding an optional parameter changes the signatures of Members(), FieldMembers() and MethodMembers(). 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 gained includeReferenced.

Tests

Three tests in DependenciesToOtherAssembliesTests, whose architecture loads only ArchUnitNETTests, so that TestAssembly types and framework types are referenced:

  • IncludeReferencedMembersTest: a called method and a read field of the referenced ClassInTestAssembly are returned by MethodMembers(true), FieldMembers(true) and Members(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, with CallAny passing and NotCallAny failing on a class that reads DateTime.Now.

With the new loader phase commented out, the first and the third test fail. dotnet test passes on both solution filters, and dotnet 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 reads DateTime.Now, because without true the 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 of NotCallAny, NotDependOnAny and 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

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-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.61%. Comparing base (f439e0d) to head (f5abea5).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to check for system members?

2 participants