Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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