Search code examples
testingjunitarchunit

Can I check for usage of lombok.experimental.* annotations with ArchUnit?"


As the question suggests, how can I check for certain imports with archUnit.

So I want the test to fail, when the tested class itself imports lombok.experimental.*.

I understand how to check for packages and stuff like that, but the approach doesnt seem to work for imports. Any suggestions?

My Code:

package com.nikita.Nikitos;

import static org.junit.Assert.assertTrue;

import org.junit.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;

public class AppTest
{
    @Test
    public void keineKlassenAusLombokExperimental() {

        JavaClasses classes = new ClassFileImporter()
                .importPackages("com.nikita..");

        noClasses().should().dependOnClassesThat()
        .resideInAPackage("lombok.experimental..").check(classes);

    }

}

The class that I want to test:

package com.nikita.Nikitos;

import lombok.experimental.UtilityClass;

@UtilityClass
public class App
{
static int hd;
}

Solution

  • Lombok acts as an annotation processor that modifies your class.

    In case of @lombok.experimental.UtilityClass (and probably other lombok annotations as well), the final byte code doesn't actually contain the annotation anymore:

    @lombok.experimental.UtilityClass
    public class App {
        static int hd;
    }
    

    is compiled (transformed) to

    public final class App
      flags: (0x0031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER
      this_class: #5                          // App
      super_class: #6                         // java/lang/Object
      interfaces: 0, fields: 1, methods: 1, attributes: 1
    Constant pool:
       #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
       #2 = Class              #16            // java/lang/UnsupportedOperationException
       #3 = String             #17            // This is a utility class and cannot be instantiated
       #4 = Methodref          #2.#18         // java/lang/UnsupportedOperationException."<init>":(Ljava/lang/String;)V
       #5 = Class              #19            // App
       #6 = Class              #20            // java/lang/Object
       #7 = Utf8               hd
       #8 = Utf8               I
       #9 = Utf8               <init>
      #10 = Utf8               ()V
      #11 = Utf8               Code
      #12 = Utf8               LineNumberTable
      #13 = Utf8               SourceFile
      #14 = Utf8               App.java
      #15 = NameAndType        #9:#10         // "<init>":()V
      #16 = Utf8               java/lang/UnsupportedOperationException
      #17 = Utf8               This is a utility class and cannot be instantiated
      #18 = NameAndType        #9:#21         // "<init>":(Ljava/lang/String;)V
      #19 = Utf8               App
      #20 = Utf8               java/lang/Object
      #21 = Utf8               (Ljava/lang/String;)V
    {
      static int hd;
        descriptor: I
        flags: (0x0008) ACC_STATIC
    
      private App();
        descriptor: ()V
        flags: (0x0002) ACC_PRIVATE
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: new           #2                  // class java/lang/UnsupportedOperationException
             7: dup
             8: ldc           #3                  // String This is a utility class and cannot be instantiated
            10: invokespecial #4                  // Method java/lang/UnsupportedOperationException."<init>":(Ljava/lang/String;)V
            13: athrow
          LineNumberTable:
            line 4: 0
    }
    

    which could also have been produced from this plain Java code:

    public final class App {
        static int hd;
    
        private App() {
            throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
        }
    }
    

    If you want to detect such a pattern in the bytecode with ArchUnit, you'd probably have to reverse-engineer what Lombok does and e.g. search for private constructors in final classes that call the UnsupportedOperationException(String) constructor:

    ArchRule no_UtilityClass = noConstructors()
        .should().bePrivate()
        .andShould().beDeclaredInClassesThat().haveModifier(JavaModifier.FINAL)
        .andShould(new ArchCondition<JavaCodeUnit>("call new UnsupportedOperationException(String)") {
            @Override
            public void check(JavaCodeUnit codeUnit, ConditionEvents events) {
                boolean satisfied = codeUnit.getCallsFromSelf().stream().anyMatch(call ->
                        call.getTargetOwner().isEquivalentTo(UnsupportedOperationException.class)
                     && call.getName().equals(JavaConstructor.CONSTRUCTOR_NAME)
                     && call.getTarget().getRawParameterTypes().size() == 1
                     && call.getTarget().getRawParameterTypes().get(0).isEquivalentTo(String.class)
                );
                String message = String.format("%s %s `new UnsupportedOperationException(String)` in %s",
                        codeUnit.getDescription(), satisfied ? "calls" : "does not call", codeUnit.getSourceCodeLocation()
                );
                events.add(new SimpleConditionEvent(codeUnit, satisfied, message));
            }
        });
    

    If you instead want to forbid the usage of lombok.experimental.* in the source code, you'll unfortunately need another tool; ArchUnit (currently) only analyzes bytecode.