Skip to content

Commit

Permalink
Support Java package-level annotations (#175)
Browse files Browse the repository at this point in the history
* Support Java package-level annotations.

Closes #156
  • Loading branch information
fzhinkin authored Jan 29, 2024
1 parent 3969489 commit 0507790
Show file tree
Hide file tree
Showing 24 changed files with 254 additions and 4 deletions.
1 change: 1 addition & 0 deletions api/binary-compatibility-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public final class kotlinx/validation/api/ClassBinarySignature {
public final class kotlinx/validation/api/KotlinSignaturesLoadingKt {
public static final fun dump (Ljava/util/List;)Ljava/io/PrintStream;
public static final fun dump (Ljava/util/List;Ljava/lang/Appendable;)Ljava/lang/Appendable;
public static final fun extractAnnotatedPackages (Ljava/util/List;Ljava/util/Set;)Ljava/util/List;
public static final fun filterOutAnnotated (Ljava/util/List;Ljava/util/Set;)Ljava/util/List;
public static final fun filterOutNonPublic (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;)Ljava/util/List;
public static synthetic fun filterOutNonPublic$default (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;ILjava/lang/Object;)Ljava/util/List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
package kotlinx.validation.test

import kotlinx.validation.api.*
import org.assertj.core.api.Assertions
import org.junit.*
import kotlin.test.assertTrue

class NonPublicMarkersTest : BaseKotlinGradleTest() {

Expand Down Expand Up @@ -35,4 +37,38 @@ class NonPublicMarkersTest : BaseKotlinGradleTest() {
assertTaskSuccess(":apiCheck")
}
}

@Test
fun testFiltrationByPackageLevelAnnotations() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/nonPublicMarkers/packages.gradle.kts")
}
java("annotated/PackageAnnotation.java") {
resolve("/examples/classes/PackageAnnotation.java")
}
java("annotated/package-info.java") {
resolve("/examples/classes/package-info.java")
}
kotlin("ClassFromAnnotatedPackage.kt") {
resolve("/examples/classes/ClassFromAnnotatedPackage.kt")
}
kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}
runner {
arguments.add(":apiDump")
}
}

runner.build().apply {
assertTaskSuccess(":apiDump")

assertTrue(rootProjectApiDump.exists(), "api dump file should exist")

val expected = readFileList("/examples/classes/AnotherBuildConfig.dump")
Assertions.assertThat(rootProjectApiDump.readText()).isEqualToIgnoringNewLines(expected)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import kotlinx.validation.api.buildGradleKts
import kotlinx.validation.api.kotlin
import kotlinx.validation.api.resolve
import kotlinx.validation.api.test
import org.assertj.core.api.Assertions
import org.junit.Test
import kotlin.test.assertTrue

class PublicMarkersTest : BaseKotlinGradleTest() {

Expand Down Expand Up @@ -43,4 +45,38 @@ class PublicMarkersTest : BaseKotlinGradleTest() {
assertTaskSuccess(":apiCheck")
}
}

@Test
fun testFiltrationByPackageLevelAnnotations() {
val runner = test {
buildGradleKts {
resolve("/examples/gradle/base/withPlugin.gradle.kts")
resolve("/examples/gradle/configuration/publicMarkers/packages.gradle.kts")
}
java("annotated/PackageAnnotation.java") {
resolve("/examples/classes/PackageAnnotation.java")
}
java("annotated/package-info.java") {
resolve("/examples/classes/package-info.java")
}
kotlin("ClassFromAnnotatedPackage.kt") {
resolve("/examples/classes/ClassFromAnnotatedPackage.kt")
}
kotlin("AnotherBuildConfig.kt") {
resolve("/examples/classes/AnotherBuildConfig.kt")
}
runner {
arguments.add(":apiDump")
}
}

runner.build().apply {
assertTaskSuccess(":apiDump")

assertTrue(rootProjectApiDump.exists(), "api dump file should exist")

val expected = readFileList("/examples/classes/AnnotatedPackage.dump")
Assertions.assertThat(rootProjectApiDump.readText()).isEqualToIgnoringNewLines(expected)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public final class annotated/ClassFromAnnotatedPackage {
public fun <init> ()V
}

public abstract interface annotation class annotated/PackageAnnotation : java/lang/annotation/Annotation {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package annotated

class ClassFromAnnotatedPackage {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package annotated;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PACKAGE)
public @interface PackageAnnotation {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@PackageAnnotation
package annotated;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

configure<kotlinx.validation.ApiValidationExtension> {
nonPublicMarkers.add("annotated.PackageAnnotation")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

configure<kotlinx.validation.ApiValidationExtension> {
publicMarkers.add("annotated.PackageAnnotation")
}
7 changes: 5 additions & 2 deletions src/main/kotlin/KotlinApiBuildTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,13 @@ public open class KotlinApiBuildTask @Inject constructor(
throw GradleException("KotlinApiBuildTask should have either inputClassesDirs, or inputJar property set")
}

val publicPackagesNames = signatures.extractAnnotatedPackages(publicMarkers.map(::replaceDots).toSet())
val ignoredPackagesNames = signatures.extractAnnotatedPackages(nonPublicMarkers.map(::replaceDots).toSet())

val filteredSignatures = signatures
.retainExplicitlyIncludedIfDeclared(publicPackages, publicClasses, publicMarkers)
.filterOutNonPublic(ignoredPackages, ignoredClasses)
.retainExplicitlyIncludedIfDeclared(publicPackages + publicPackagesNames,
publicClasses, publicMarkers)
.filterOutNonPublic(ignoredPackages + ignoredPackagesNames, ignoredClasses)
.filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet())

outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer ->
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/api/AsmMetadataLoading.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ internal fun isProtected(access: Int) = access and Opcodes.ACC_PROTECTED != 0
internal fun isStatic(access: Int) = access and Opcodes.ACC_STATIC != 0
internal fun isFinal(access: Int) = access and Opcodes.ACC_FINAL != 0
internal fun isSynthetic(access: Int) = access and Opcodes.ACC_SYNTHETIC != 0
internal fun isAbstract(access: Int) = access and Opcodes.ACC_ABSTRACT != 0
internal fun isInterface(access: Int) = access and Opcodes.ACC_INTERFACE != 0

internal fun ClassNode.isEffectivelyPublic(classVisibility: ClassVisibility?) =
isPublic(access)
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/api/KotlinMetadataSignature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ internal data class AccessFlags(val access: Int) {
val isStatic: Boolean get() = isStatic(access)
val isFinal: Boolean get() = isFinal(access)
val isSynthetic: Boolean get() = isSynthetic(access)
val isAbstract: Boolean get() = isAbstract(access)
val isInterface: Boolean get() = isInterface(access)

private fun getModifiers(): List<String> =
ACCESS_NAMES.entries.mapNotNull { if (access and it.key != 0) it.value else null }
Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/api/KotlinSignaturesLoading.kt
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,33 @@ private fun List<ClassBinarySignature>.filterOutNotAnnotated(
}
}

/**
* Extracts name of packages annotated by one of the [targetAnnotations].
* If there are no such packages, returns an empty list.
*
* Package is checked for being annotated by looking at classes with `package-info` name
* ([see JSL 7.4.1](https://docs.oracle.com/javase/specs/jls/se21/html/jls-7.html#jls-7.4)
* for details about `package-info`).
*/
@ExternalApi
public fun List<ClassBinarySignature>.extractAnnotatedPackages(targetAnnotations: Set<String>): List<String> {
if (targetAnnotations.isEmpty()) return emptyList()

return filter {
it.name.endsWith("/package-info")
}.filter {
// package-info classes are private synthetic abstract interfaces since 2005 (JDK-6232928).
it.access.isInterface && it.access.isSynthetic && it.access.isAbstract
}.filter {
it.annotations.any {
ann -> targetAnnotations.any { ann.refersToName(it) }
}
}.map {
val res = it.name.substring(0, it.name.length - "/package-info".length)
res
}
}

@ExternalApi
public fun List<ClassBinarySignature>.filterOutNonPublic(
nonPublicPackages: Collection<String> = emptyList(),
Expand Down
9 changes: 9 additions & 0 deletions src/test/kotlin/cases/packageAnnotations/PrivateApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations

@PrivateApi
annotation class PrivateApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations.a.a

public class ShouldBeDeleted {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations.a.b

public class ShouldBeDeleted {
}
9 changes: 9 additions & 0 deletions src/test/kotlin/cases/packageAnnotations/a/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

@PrivateApi
package cases.packageAnnotations.a;

import cases.packageAnnotations.PrivateApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations.b.a

public class ShouldBeDeleted {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

@PrivateApi @Deprecated
package cases.packageAnnotations.b.a;

import cases.packageAnnotations.PrivateApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations.b.b

public class ShouldRemainPublic {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright 2016-2023 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations.b.b;
14 changes: 14 additions & 0 deletions src/test/kotlin/cases/packageAnnotations/b/c/shouldRemainPublic.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright 2016-2024 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package cases.packageAnnotations.b.c

import cases.packageAnnotations.PrivateApi

@PrivateApi
interface `package-info` {
}

class ShouldNotBeRemoved
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public final class cases/packageAnnotations/b/b/ShouldRemainPublic {
public fun <init> ()V
}

public final class cases/packageAnnotations/b/c/ShouldNotBeRemoved {
public fun <init> ()V
}

12 changes: 10 additions & 2 deletions src/test/kotlin/tests/CasesPublicAPITest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import kotlinx.validation.api.*
import org.junit.*
import org.junit.rules.TestName
import java.io.File
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.walk

class CasesPublicAPITest {

Expand Down Expand Up @@ -45,6 +48,8 @@ class CasesPublicAPITest {

@Test fun nestedClasses() { snapshotAPIAndCompare(testName.methodName) }

@Test fun packageAnnotations() { snapshotAPIAndCompare(testName.methodName, setOf("cases/packageAnnotations/PrivateApi")) }

@Test fun private() { snapshotAPIAndCompare(testName.methodName) }

@Test fun protected() { snapshotAPIAndCompare(testName.methodName) }
Expand All @@ -57,13 +62,16 @@ class CasesPublicAPITest {

@Test fun enums() { snapshotAPIAndCompare(testName.methodName) }

@OptIn(ExperimentalPathApi::class)
private fun snapshotAPIAndCompare(testClassRelativePath: String, nonPublicMarkers: Set<String> = emptySet()) {
val testClassPaths = baseClassPaths.map { it.resolve(testClassRelativePath) }
val testClasses = testClassPaths.flatMap { it.listFiles().orEmpty().asIterable() }
val testClasses = testClassPaths.flatMap { it.toPath().walk().map(Path::toFile) }
check(testClasses.isNotEmpty()) { "No class files are found in paths: $testClassPaths" }

val testClassStreams = testClasses.asSequence().filter { it.name.endsWith(".class") }.map { it.inputStream() }
val api = testClassStreams.loadApiFromJvmClasses().filterOutNonPublic().filterOutAnnotated(nonPublicMarkers)
val classes = testClassStreams.loadApiFromJvmClasses()
val additionalPackages = classes.extractAnnotatedPackages(nonPublicMarkers)
val api = classes.filterOutNonPublic(nonPublicPackages = additionalPackages).filterOutAnnotated(nonPublicMarkers)
val target = baseOutputPath.resolve(testClassRelativePath).resolve(testName.methodName + ".txt")
api.dumpAndCompareWith(target)
}
Expand Down

0 comments on commit 0507790

Please sign in to comment.