javaarchunit

Check if a field is initialized with ArchUnit


I have a class that looks like this:

public class A {

   private B b1 = new B();

   private C c = new C();

   private B b2;

}

I want to write an ArchUnit test that finds all the fields in class A that are of type B and that are initialized. So in this case, just field 'b1'.

I wrote a test like this, but can't finish it. Is this even possible?

 @ArchTest
  void dontInitialize(JavaClasses classesToTest) {
    noFields().that().haveRawType(DescribedPredicate.describe("that is a B object", type -> {
      return type.isAssignableTo(B.class);
    })).should(new ArchCondition<JavaField>("") {
      @Override
      public void check(JavaField javaField, ConditionEvents conditionEvents) {
        // ???
      }
    });
  }

Solution

  • At the byte code level, field initialization is moved into the constructor. For your example class A:

      private B b1;
        descriptor: LB;
        flags: (0x0002) ACC_PRIVATE
    
      private C c;
        descriptor: LC;
        flags: (0x0002) ACC_PRIVATE
    
      private B b2;
        descriptor: LB;
        flags: (0x0002) ACC_PRIVATE
    
      public A();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: new           #7                  // class B
             8: dup
             9: invokespecial #9                  // Method B."<init>":()V
            12: putfield      #10                 // Field b1:LB;
            15: aload_0
            16: new           #16                 // class C
            19: dup
            20: invokespecial #18                 // Method C."<init>":()V
            23: putfield      #19                 // Field c:LC;
            26: return
    

    So you can have an ArchRule that tests for setting accesses from a constructor, e.g.:

    fields().should(be(describe("set in a constructor", field ->
        field.getAccessesToSelf().stream().anyMatch(access ->
            access.getOwner().isConstructor() &&
            access.getAccessType() == JavaFieldAccess.AccessType.SET
        )
    )));
    

    using the following static imports:

    import static com.tngtech.archunit.base.DescribedPredicate.describe;
    import static com.tngtech.archunit.lang.conditions.ArchConditions.be;
    import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
    

    This simple rule can have false negatives when multiple constructors are involved, e.g.

    class A {
        int i1;
        int i2;
    
        A() {
            i1 = 0;
        }
    
        A(int i2) {
            this.i2 = i2;
        }
    }
    

    but you can probably work out a smarter rule based on this idea.