Search code examples
javaarchunit

ArchUnit: Check only field method-calls


I need to make sure that classes annotated by @Path only call methods in classes annotated by @Secured. But only method-calls on fields (e.g. injected beans) should be checked. What I have so far is:

ArchRuleDefinition.classes().that()
        .areAnnotatedWith(Path.class)
        .or().areMetaAnnotatedWith(Path.class)
        .should().onlyCallMethodsThat(are(declaredIn(!!!))

So 2 parts are missing:

  • The check that only calls on fields should be considered and not the ones on local variables
  • The part in declaredIn to check that the methods should be declared in a class that has @Secured-annotation

Is there a way to achieve this?


Solution

  • There's a simple solution for the second part: With

    import com.tngtech.archunit.core.domain.JavaClass;
    import com.tngtech.archunit.core.domain.properties.HasOwner.Functions.Get;
    
    import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
    import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
    

    you can use

        classes()
                .that().areMetaAnnotatedWith(Path.class)
                .should().onlyCallMethodsThat(
                        Get.<JavaClass>owner().is(annotatedWith(Secured.class))
                                .as("are defined in classes annotated with @Secured")
                );
    

    Note that areMetaAnnotatedWith also includes direct annoations since ArchUnit 0.17.0. If you wanted to test constructor calls as well, you could replace onlyCallMethodsThat with onlyCallCodeUnitsThat.


    Unfortunately, I don't think that the first part can be solved with the current version ArchUnit 0.23.1: You can rewrite the above rule to test each JavaMethodCall, which is more specific than the called JavaMethod:

        noClasses()
                .that().areMetaAnnotatedWith(Path.class)
                .should().callMethodWhere(describe("owner is not @Secured",
                    methodCall ->
                        !methodCall.getTargetOwner().isAnnotatedWith(Secured.class)
                ));
    

    With JavaMethodCall#getOrigin(), you can find the JavaCodeUnit where the method call occured, but this does not include information about the object on which the method was called.

    If I understood correctly, you want to distinguish the following cases:

    @Secured
    class SecuredOwner {
        void method() {
        }
    }
    
    class NotSecuredOwner {
        void method() {
        }
    }
    
    @Path
    class ThisShouldBeOkay {
        SecuredOwner securedOwner = new SecuredOwner();
    
        void call_secured_method_on_field() {
            securedOwner.method();
        }
    
        void call_not_secured_method_on_local_variable() {
            new NotSecuredOwner().method();
        }
    }