Issue
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.
Answered By - Manfred
Answer Checked By - Mary Flores (JavaFixing Volunteer)