javaarchunit

Enforcing least visibility modifier with ArchUnit in Java


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'.


Solution

  • 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())