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 1:

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.

Solution 2:

An import does not generate any signature in the bytecode, so ArchUnit cannot detect it directly.
Isn't it sufficient to check that your code does not depend on that package?

ArchRule lombok_experimental_is_not_used = noClasses()
        .should().dependOnClassesThat().resideInAPackage("lombok.experimental..");

If you only wanted to detect the star import, then you unfortunately need to use another tool.