Sub interface dependency not discovered if caller only uses super interface methods
I have a com.example.child.ChildInterface extending a com.example.root.RootInterface. RootInterface provides a someRootMethod() and ChildInterface adds an extra someChildMethod().
Now let's say I have 2 usages of the ChildInterface:
-
com.example.usage1.RootMethodUsageonly uses thesomeRootMethod() -
com.example.usage2.ChildMethodUsageonly uses thesomeChildMethod()
And I want to detect that RootMethodUsage should not use ChildInterface but RootInterface instead.
So I declared 2 rules for that:
@ArchTest
public static final ArchRule rootUsage = classes().that().resideInAPackage("com.example.root..")
.should().onlyBeAccessed().byClassesThat().resideInAPackage("com.example.usage1..");
@ArchTest
public static final ArchRule childUsage = classes().that().resideInAPackage("com.example.child..")
.should().onlyBeAccessed().byClassesThat().resideInAPackage("com.example.usage2..");
But those tests run correctly, particularly childUsage that should fail, given that RootMethodUsage classes indeed uses a ChildInterface occurrence, even if it uses only the RootInterface's method. I can only detect an error if RootMethodUsage also uses someChildMethod().
Is there a solution for me to get the expected behavior, that is detect that ChildInterface should not be used by RootMethodUsage?
Complete example available at: https://github.com/Alexandre-Carbenay/archunit-interfaces
A more concrete example would be a super interface for Repository in a common package, and some domain package x containing XRepository interface extending Repository. I would like to ensure that only services in x package use the XRepository interface, and services in y and z packages use XService instead of accessing XRepository. Ideally, XRepository should be package private, except that implementation may be in different packages, so it needs to be public.
Problem is, when YService uses only methods declared in Repository, even if referencing a XRepository, I cannot detect it with ArchUnit.
Hmm, I think actually accessTo and accessFrom behave a little inconsistent here :frowning:
accessFrom checks if the call target resolves to this method, i.e. where the method is defined that actually will be called. And that is not ChildInterface, but RootInterface.
However, if you write the method with accessTo and doubly negated:
@ArchTest
public static final ArchRule childUsage = noClasses()
.that().resideOutsideOfPackage("com.example.usage2..")
.should().accessClassesThat().resideInAPackage("com.example.child..");
you will get the expected violation. Maybe it would make more sense to adjust accessFrom to follow the same behavior here. It depends a little, if in the case
interface Root {
void foo();
}
interface Child extends Root {}
class Caller {
void call(Child child) {
child.foo();
}
}
Does Caller call a method on Child or on Root? In the end Child on a local perspective does not know anything about foo(), on the other hand the type we call it on is Child.
In any case, I think accessFrom and accessTo should follow the same semantics and probably the way that counts the method to belong to Child.
For your immediate case you could use that doubly negated form to have the behavior. You could also ponder if it is not onlyHaveDependentClassesThat() that you want? Are you really only interested in accesses? Because if you forbid all dependencies in general, you wouldn't even come into this situation, since the field type would already be a violation.