Enforcing least visibility modifier with ArchUnit in Java

52 Views Asked by At

I'd like to enforce a policy in my project, that if something is made public then it must be used from another package. I'd like to enforce this for pretty much everything - classes, interfaces, methods...

    methods()
        .that()
        .arePublic()
        .should()
        .onlyBeCalled()
        .byClassesThat()
        .resideOutsideOfPackage(/*...*/);

However, resideOutsideOfPackage requires a specific String packageName whereas, I'd like to use the package name of the currently traversed method's class'.

1

There are 1 best solutions below

0
Roland Weisleder On BEST ANSWER

In this case you need to implement your own ArchCondition, I have prepared a skeleton that you can adapt to your needs.

ArchRule enforceLeastRequiredModifier = CompositeArchRule.of(
    classes().should(haveLeastRequiredModifier())
).and(
    members().should(haveLeastRequiredModifier())
);

private static <T extends HasModifiers & HasDescription & HasSourceCodeLocation> ArchCondition<T> haveLeastRequiredModifier() {
    return new ArchCondition<T>("have least required modifier") {
        @Override
        public void check(T item, ConditionEvents events) {
            boolean isPublic = item.getModifiers().contains(PUBLIC);
            boolean isProtected = item.getModifiers().contains(PROTECTED);
            boolean isPrivate = item.getModifiers().contains(PRIVATE);
            boolean isPackagePrivate = !isPublic && !isProtected && !isPrivate;

            if (isPublic) {
                boolean isUsedInOtherPackage;
                if (item instanceof JavaClass) {
                    JavaClass javaClass = (JavaClass) item;
                    isUsedInOtherPackage = javaClass.getAccessesToSelf().stream().anyMatch(access -> !access.getOriginOwner().getPackageName().equals(javaClass.getPackageName()));
                } else {
                    JavaMember javaMember = (JavaMember) item;
                    JavaClass memberOwner = javaMember.getOwner();
                    isUsedInOtherPackage = javaMember.getAccessesToSelf().stream().anyMatch(access -> !access.getOriginOwner().getPackageName().equals(memberOwner.getPackageName()));
                }

                if (isUsedInOtherPackage) {
                    events.add(satisfied(item, createMessage(item, "is used in other package")));
                } else {
                    events.add(violated(item, createMessage(item, "could be package-private")));
                }
            }

            // TODO if needed, do the same for isProtected and isPackagePrivate
        }
    };
}

Or alternatively split the big condition in smaller ones by writing rules like

classes().that().arePublic().should(beAccessedFromOtherPackage())