Kotlin is an awesome language that enables developers to write more concise and safer code. However, it has some weak spots [1] [2]
Another of its pain points is that Kotlin lacks real package-private scope and internal
visibility modifier in Kotlin is weaker than the package-private access level in Java. [3]
In Java, class marked with package-private access level is visible only within the same package. On the other hand, Kotlin's internal modifier makes class visible within the same source set of files compiled together. This makes an internal modifier much broader than its Java counterpart.
Using an internal modifier and creating a separate module is cumbersome compared to using package scope back in Java.
So maybe we can make an internal modifier more strict?
It turns out we can use the ArchUnit library to achieve that goal. ArchUnit checks the architecture of code during unit tests and can be easily extended using custom rules.
We can create a rule that finds all internal Kotlin classes in our application and check if they're accessing other packages.
Let's start with checking if we deal with Kotlin class and whether this class is internal. Let's define two extension functions on Class
:
private fun Class<*>.isKotlinClass() = this.declaredAnnotations.any {
it.annotationClass.qualifiedName == "kotlin.Metadata"
}
private fun Class<*>.isInternal() = this.kotlin.visibility == KVisibility.INTERNAL
Then, we need to perform two checks:
a) no public classes are accessing internal classes residing in subpackages
b) no internal classes are accessing classes residing in outer packages
Having Kotlin internal detection present and both rules we can wire everything together using ArchUnit powered unit tests:
@Test
fun `limit kotlin-internal scope`(){
val nonInternalKotlinClasses = ClassFileImporter()
.importPackages("com.acme")
.that(isKotlinNotInternal())
noClasses()
.should(accessClassesThatResideInASubpackage())
.check(nonInternalKotlinClasses)
val internalKotlinClasses = ClassFileImporter()
.importPackages("com.acme")
.that(isKotlinInternal())
noClasses()
.should(accessClassesThatResideInAnOuterPackage())
.check(internalKotlinClasses)
}
The downside of using ArchUnit is that you know that something went wrong during the tests instead of compile time. Although by applying those checks you are sure that internal classes are subject to more strict rules than offered by Kotlin.
Using ArchUnit to perform internal visibility checks together with improved IDE support will greatly help in building modularized Kotlin applications.
Source code for checks and example project can be found here: https://github.com/kkocel/archunit-kotlin-internal