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.