ArchUnit icon indicating copy to clipboard operation
ArchUnit copied to clipboard

How to get list of inner classes from JavaClass

Open marvk opened this issue 3 years ago • 6 comments

I'm trying to write an ArchCondition that checks if all inner classes of a class have the same annotation, but with distinct values. However, I fail to see how I can access a list of inner classes of a JavaClass.

I've tried JavaClass.members and JavaClass.allMembers without result. I've scrolled through the list of fields and methods on JavaClass, but I can't seem to find the right one if it exists.

Could someone point me in the right direction?

Thanks in advance!

marvk avatar Oct 04 '22 18:10 marvk

JavaClass.getAllSubclasses() might solve your issue, I am using 0.23.1.

sann3 avatar Oct 07 '22 15:10 sann3

I'm not aware that ArchUnit provides a direct API to obtain all inner / nested classes (note that there's a subtle difference) of a given JavaClass, but you can easily collect them from all imported JavaClasses:

@AnalyzeClasses(packagesOf = {Issue974.class})
class Issue974 {

    @ArchTest
    void nestedClassesShouldHaveDistinctAnnotations(JavaClasses classes) {
        Class<Issue974> outerClass = Issue974.class;

        Set<JavaClass> nestedClasses = classes.stream()
                .filter(javaClass -> javaClass.getEnclosingClass()
                        .map(enclosingClass -> enclosingClass.isEquivalentTo(outerClass))
                        .orElse(false)
                )
                .collect(toSet());

        Set<String> annotationValues = nestedClasses.stream()
                .flatMap(javaClass -> javaClass.tryGetAnnotationOfType(Annotation.class)
                        .map(Annotation::value)
                        .asSet()
                        .stream()
                )
                .collect(toSet());
        
        assertThat(nestedClasses)
                .as("nested classes of " + outerClass)
                .hasSameSizeAs(annotationValues);
    }

    @Annotation("I1")
    class I1 {
    }

    @Annotation("I2")
    class I2 {
    }
}

@interface Annotation {
    String value();
}

(I'm using AssertJ's .hasSameSizeAs.)

hankem avatar Oct 07 '22 16:10 hankem

I'm afraid this is not that convenient at the moment :thinking: Because as mentioned the link JavaClass -> inner class is missing (I actually planned on adding it, but haven't found the time yet). If you want to solve this, then as @hankem pointed out your best bet likely is to use the link JavaClass -> enclosing class, which is currently present.

If you want to write this as a standard ArchCondition it could for example look something like this:

new ArchCondition<JavaClass>("have inner classes that ...") {
  private final Map<JavaClass, Set<JavaClass>> outerClassesToInnerClasses = new HashMap<>();

  @Override
  public void init(Collection<JavaClass> allClasses) {
    allClasses.forEach(possibleInnerClass ->
        possibleInnerClass.getEnclosingClass()
            .ifPresent(outerClass -> outerClassesToInnerClasses
                .computeIfAbsent(outerClass, __ -> new HashSet<>())
                .add(possibleInnerClass)));
  }

  @Override
  public void check(JavaClass javaClass, ConditionEvents events) {
    outerClassesToInnerClasses.getOrDefault(javaClass, Collections.emptySet())
        .forEach(/* check inner classes of javaClass */);
  }
};

codecholeric avatar Oct 07 '22 17:10 codecholeric

Hey. I am trying to do a similar thing but with kotlin. I want to check that there is no class that inherits from specific class, that can have certain annotation. But I want to apply this logic also on all inner classes -> Consider nested test classes from Junit5

Consider MyClass which has additional 3 nested inner classes:

Code:

        ArchRuleDefinition
            .noClasses().that().areAssignableTo(MyClass::class.java)
            .and(
                describe("whatever") {
                    it.allSubclasses // returns empty via debugger but should not
                }
            )
            .should().dependOnClassesThat().belongToAnyOf(WithMockUser::class.java)
            .check(ArchUnit.testClasses)

thugec avatar Nov 15 '23 13:11 thugec

@thugec, can you provide a minimal example of MyClass and its subclasses, and which violations you want to catch? I'm sorry, but I didn't properly understand what you want to achieve. (Do you only want to test subclasses that are also inner classes, or both inner classes and sub classes? How does this relate to JUnit5's @Nested classes?)

hankem avatar Nov 16 '23 07:11 hankem

@thugec, can you provide a minimal example of MyClass and its subclasses, and which violations you want to catch? I'm sorry, but I didn't properly understand what you want to achieve. (Do you only want to test subclasses that are also inner classes, or both inner classes and sub classes? How does this relate to JUnit5's @Nested classes?)

I wanted to achieve that "forbidden" annotation cannot be also present on the declaration of the inner classes, not only on the top class. JavaClass returns also provide "members", but it only returns function inside a Test class, never the test functions for example. I cannot share code snippet due to company policy. Basically I am trying to find a way, how to get also list of inner classes in the kotlin for a particular top level class. I expected that:

public Set<JavaClass> getSubclasses() {
        return subclasses;
    }

does the trick, but obviously it does not.

Here is a very simple example:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MyClass {

    @Nested
    inner class TestA {}

    @Nested
    inner class TestB {}

}

I want to check if TestA and TestB does not contain certain annotations (or whatever else)

thugec avatar Nov 18 '23 16:11 thugec