ArchUnit icon indicating copy to clipboard operation
ArchUnit copied to clipboard

Differentiate objects of different languages

Open Airblader opened this issue 4 years ago • 4 comments

Currently, JavaClass (which is a slight misnomer now) handles classes of all languages (e.g. Java + Scala). There is #372 to explicitly support Scala, however this request aims more to improve the support to control which languages' objects to look at in rules.

For example, I would like to be able to apply rules only on Java classes, so something like isJava / isScala (or getLanguage with an Enum) would be great. The current workaround is to manually implement this, look at the Source and check the filename. I don't know if on byte code level there's a better way, especially since both Source and the source's filename are optional.

Airblader avatar Sep 10 '21 05:09 Airblader

Thanks for raising this! :slightly_smiling_face: Yes, I know JavaClass might be somewhat misleading, maybe something like JvmClass would be more fitting, since it's actually looking at bytecode :wink: Unfortunately I also don't know any way at the moment other than looking at the JavaClass.source :thinking: It seems like the only way to somewhat derive this... Take for example ArchUnit's own HasDescription

Classfile .../archunit/archunit/build/classes/java/main/com/tngtech/archunit/base/HasDescription.class
  Last modified Oct 24, 2021; size 442 bytes
  MD5 checksum 12407c91e13f16c5fd0a8ea9e93882a7
  Compiled from "HasDescription.java"
public interface com.tngtech.archunit.base.HasDescription
  minor version: 0
  major version: 51
  flags: (0x0601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
  this_class: #1                          // com/tngtech/archunit/base/HasDescription
  super_class: #3                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 2
Constant pool:
   #1 = Class              #2             // com/tngtech/archunit/base/HasDescription
   #2 = Utf8               com/tngtech/archunit/base/HasDescription
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               getDescription
   #6 = Utf8               ()Ljava/lang/String;
   #7 = Utf8               RuntimeInvisibleAnnotations
   #8 = Utf8               Lcom/tngtech/archunit/PublicAPI;
   #9 = Utf8               usage
  #10 = Utf8               Lcom/tngtech/archunit/PublicAPI$Usage;
  #11 = Utf8               ACCESS
  #12 = Utf8               SourceFile
  #13 = Utf8               HasDescription.java
  #14 = Utf8               InnerClasses
  #15 = Class              #16            // com/tngtech/archunit/PublicAPI$Usage
  #16 = Utf8               com/tngtech/archunit/PublicAPI$Usage
  #17 = Class              #18            // com/tngtech/archunit/PublicAPI
  #18 = Utf8               com/tngtech/archunit/PublicAPI
  #19 = Utf8               Usage
{
  public abstract java.lang.String getDescription();
    descriptor: ()Ljava/lang/String;
    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
    RuntimeInvisibleAnnotations:
      0: #8(#9=e#10.#11)
        com.tngtech.archunit.PublicAPI(
          usage=Lcom/tngtech/archunit/PublicAPI$Usage;.ACCESS
        )
}
SourceFile: "HasDescription.java"
InnerClasses:
  public static final #19= #15 of #17;    // Usage=class com/tngtech/archunit/PublicAPI$Usage of class com/tngtech/archunit/PublicAPI

Besides the SourceFile and Compiled from there is hardly any info where this bytecode comes from. There is also no hint which compiler was used, etc. So it seems like this could only be a convenience feature based on JavaClass.source that might fail though, if no source is available :thinking:

codecholeric avatar Oct 24 '21 17:10 codecholeric

That's a real shame, but I won't ask ArchUnit to do something that it can't, so feel free to close this issue if there's nothing useful to be done here. Thanks for the reply!

Airblader avatar Oct 24 '21 18:10 Airblader

I'll leave it open for a little, in hopes that some bytecode guru might magically jump in and give us the right hint :wink: But yeah, if we can't come up with any good idea within the next weeks I'll likely close it, and apologize to you :disappointed:

One ugly hack might also be dependencies.any { it.targetClass.name.startsWith("kotlin.") } :see_no_evil: Because usually I would guess any non-Kotlin class doesn't depend on the Kotlin SDK and any Kotlin class will. But of course also just a hack :disappointed:

codecholeric avatar Oct 28 '21 17:10 codecholeric

There's some ways a little less hacky than this, but each is language dependent. For example, my project has a legacy package of Groovy tests, which we want to stop from growing any larger. All classes emitted from the Groovy compiler implement groovy.lang.GroovyObject, which I use as follows

    @ArchTest
    val `no ATs may be written in Groovy` = freeze(
        noMethods()
            .that().areMetaAnnotatedWith(Testable::class.java)
            .should().beDeclaredInClassesThat().implement("groovy.lang.GroovyObject")
            .`as`("No further tests may be written in Groovy")
    )

(test is in Kotlin).

In a similar way, all classes emitted by the Kotlin compiler have a @Metadata annotation. https://www.mobileit.cz/Blog/Pages/arch-unit-4.aspx shows a way to implement a DescribedPredicate that tests for a kotlin class using the kotlinx-metadata library.

Given the number of languages targetting the JVM, I don't think an enum would be suitable - but having an extension suite of predicates etc for each language would be very useful.

winstaan74 avatar May 24 '22 14:05 winstaan74