Skip to content

Commit 5d08f51

Browse files
Support for explicit public API markers (#116)
* Add support for defining public declarations explicitly If the added properties are used, all unmatched declarations will be excluded from the API check. If properties for both ignored and explicit markers are set, filtering of ignored declarations will happen after filtering of declarations not explicitly marked as public. * Support validation of non-main source sets for kotlin-jvm projects
1 parent c595e65 commit 5d08f51

File tree

13 files changed

+357
-38
lines changed

13 files changed

+357
-38
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.validation.test
7+
8+
import kotlinx.validation.api.*
9+
import kotlinx.validation.api.buildGradleKts
10+
import kotlinx.validation.api.kotlin
11+
import kotlinx.validation.api.resolve
12+
import kotlinx.validation.api.test
13+
import org.junit.Test
14+
15+
class MixedMarkersTest : BaseKotlinGradleTest() {
16+
17+
@Test
18+
fun testMixedMarkers() {
19+
val runner = test {
20+
buildGradleKts {
21+
resolve("examples/gradle/base/withPlugin.gradle.kts")
22+
resolve("examples/gradle/configuration/publicMarkers/mixedMarkers.gradle.kts")
23+
}
24+
25+
kotlin("MixedAnnotations.kt") {
26+
resolve("examples/classes/MixedAnnotations.kt")
27+
}
28+
29+
apiFile(projectName = rootProjectDir.name) {
30+
resolve("examples/classes/MixedAnnotations.dump")
31+
}
32+
33+
runner {
34+
arguments.add(":apiCheck")
35+
}
36+
}
37+
38+
runner.withDebug(true).build().apply {
39+
assertTaskSuccess(":apiCheck")
40+
}
41+
}
42+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.validation.test
7+
8+
import kotlinx.validation.api.*
9+
import kotlinx.validation.api.buildGradleKts
10+
import kotlinx.validation.api.kotlin
11+
import kotlinx.validation.api.resolve
12+
import kotlinx.validation.api.test
13+
import org.junit.Test
14+
15+
class PublicMarkersTest : BaseKotlinGradleTest() {
16+
17+
@Test
18+
fun testPublicMarkers() {
19+
val runner = test {
20+
buildGradleKts {
21+
resolve("examples/gradle/base/withPlugin.gradle.kts")
22+
resolve("examples/gradle/configuration/publicMarkers/markers.gradle.kts")
23+
}
24+
25+
kotlin("ClassWithPublicMarkers.kt") {
26+
resolve("examples/classes/ClassWithPublicMarkers.kt")
27+
}
28+
29+
kotlin("ClassInPublicPackage.kt") {
30+
resolve("examples/classes/ClassInPublicPackage.kt")
31+
}
32+
33+
apiFile(projectName = rootProjectDir.name) {
34+
resolve("examples/classes/ClassWithPublicMarkers.dump")
35+
}
36+
37+
runner {
38+
arguments.add(":apiCheck")
39+
}
40+
}
41+
42+
runner.withDebug(true).build().apply {
43+
assertTaskSuccess(":apiCheck")
44+
}
45+
}
46+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package foo.api
7+
8+
class ClassInPublicPackage {
9+
class Inner
10+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
public final class foo/ClassWithPublicMarkers {
2+
public final fun getBar1 ()I
3+
public final fun getBar2 ()I
4+
public final fun setBar1 (I)V
5+
public final fun setBar2 (I)V
6+
}
7+
8+
public final class foo/ClassWithPublicMarkers$MarkedClass {
9+
public fun <init> ()V
10+
public final fun getBar1 ()I
11+
}
12+
13+
public abstract interface annotation class foo/PublicClass : java/lang/annotation/Annotation {
14+
}
15+
16+
public final class foo/api/ClassInPublicPackage {
17+
public fun <init> ()V
18+
}
19+
20+
public final class foo/api/ClassInPublicPackage$Inner {
21+
public fun <init> ()V
22+
}
23+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package foo
7+
8+
@Target(AnnotationTarget.CLASS)
9+
annotation class PublicClass
10+
11+
@Target(AnnotationTarget.FIELD)
12+
annotation class PublicField
13+
14+
@Target(AnnotationTarget.PROPERTY)
15+
annotation class PublicProperty
16+
17+
public class ClassWithPublicMarkers {
18+
@PublicField
19+
var bar1 = 42
20+
21+
@PublicProperty
22+
var bar2 = 42
23+
24+
@PublicClass
25+
class MarkedClass {
26+
val bar1 = 41
27+
}
28+
29+
var notMarkedPublic = 42
30+
31+
class NotMarkedClass
32+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public final class mixed/MarkedPublicWithPrivateMembers {
2+
public fun <init> ()V
3+
public final fun otherFun ()V
4+
}
5+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package mixed
7+
8+
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
9+
annotation class PublicApi
10+
11+
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
12+
annotation class PrivateApi
13+
14+
@PublicApi
15+
class MarkedPublicWithPrivateMembers {
16+
@PrivateApi
17+
var private1 = 42
18+
19+
@field:PrivateApi
20+
var private2 = 15
21+
22+
@PrivateApi
23+
fun privateFun() = Unit
24+
25+
@PublicApi
26+
@PrivateApi
27+
fun privateFun2() = Unit
28+
29+
fun otherFun() = Unit
30+
}
31+
32+
// Member annotations should be ignored in explicitly private classes
33+
@PrivateApi
34+
class MarkedPrivateWithPublicMembers {
35+
@PublicApi
36+
var public1 = 42
37+
38+
@field:PublicApi
39+
var public2 = 15
40+
41+
@PublicApi
42+
fun publicFun() = Unit
43+
44+
fun otherFun() = Unit
45+
}
46+
47+
@PrivateApi
48+
@PublicApi
49+
class PublicAndPrivateFilteredOut
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
configure<kotlinx.validation.ApiValidationExtension> {
7+
publicMarkers.add("foo.PublicClass")
8+
publicMarkers.add("foo.PublicField")
9+
publicMarkers.add("foo.PublicProperty")
10+
11+
publicPackages.add("foo.api")
12+
publicClasses.add("foo.PublicClass")
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright 2016-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
configure<kotlinx.validation.ApiValidationExtension> {
7+
nonPublicMarkers.add("mixed.PrivateApi")
8+
publicMarkers.add("mixed.PublicApi")
9+
}

src/main/kotlin/ApiValidationExtension.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,34 @@ open class ApiValidationExtension {
3434
* Example of such a class could be `com.package.android.BuildConfig`.
3535
*/
3636
public var ignoredClasses: MutableSet<String> = HashSet()
37+
38+
/**
39+
* Fully qualified names of annotations that can be used to explicitly mark public declarations.
40+
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
41+
* all declarations not covered by any of them will be considered non-public.
42+
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
43+
*/
44+
public var publicMarkers: MutableSet<String> = HashSet()
45+
46+
/**
47+
* Fully qualified package names that contain public declarations.
48+
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
49+
* all declarations not covered by any of them will be considered non-public.
50+
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
51+
*/
52+
public var publicPackages: MutableSet<String> = HashSet()
53+
54+
/**
55+
* Fully qualified names of public classes.
56+
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
57+
* all declarations not covered by any of them will be considered non-public.
58+
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
59+
*/
60+
public var publicClasses: MutableSet<String> = HashSet()
61+
62+
/**
63+
* Non-default Gradle SourceSet names that should be validated.
64+
* By default, only the `main` source set is checked.
65+
*/
66+
public var additionalSourceSets: MutableSet<String> = HashSet()
3767
}

src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,7 @@ class BinaryCompatibilityValidatorPlugin : Plugin<Project> {
130130
project: Project,
131131
extension: ApiValidationExtension
132132
) = configurePlugin("kotlin", project, extension) {
133-
project.sourceSets.all { sourceSet ->
134-
if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) {
135-
return@all
136-
}
137-
project.configureApiTasks(sourceSet, extension, TargetConfig(project))
138-
}
133+
project.configureApiTasks(extension, TargetConfig(project))
139134
}
140135
}
141136

@@ -225,19 +220,24 @@ fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boo
225220
projectName !in extension.ignoredProjects && !extension.validationDisabled
226221

227222
private fun Project.configureApiTasks(
228-
sourceSet: SourceSet,
229223
extension: ApiValidationExtension,
230224
targetConfig: TargetConfig = TargetConfig(this),
231225
) {
232226
val projectName = project.name
233227
val apiBuildDir = targetConfig.apiDir.map { buildDir.resolve(it) }
228+
val sourceSetsOutputsProvider = project.provider {
229+
sourceSets
230+
.filter { it.name == SourceSet.MAIN_SOURCE_SET_NAME || it.name in extension.additionalSourceSets }
231+
.map { it.output.classesDirs }
232+
}
233+
234234
val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build")) {
235235
isEnabled = apiCheckEnabled(projectName, extension)
236236
// 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
237237
description =
238238
"Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually"
239-
inputClassesDirs = files(provider<Any> { if (isEnabled) sourceSet.output.classesDirs else emptyList<Any>() })
240-
inputDependencies = files(provider<Any> { if (isEnabled) sourceSet.output.classesDirs else emptyList<Any>() })
239+
inputClassesDirs = files(provider<Any> { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList<Any>() })
240+
inputDependencies = files(provider<Any> { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList<Any>() })
241241
outputApiDir = apiBuildDir.get()
242242
}
243243

src/main/kotlin/KotlinApiBuildTask.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ open class KotlinApiBuildTask @Inject constructor(
5353
get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet()
5454
set(value) { _ignoredClasses = value }
5555

56+
private var _publicPackages: Set<String>? = null
57+
@get:Input
58+
var publicPackages: Set<String>
59+
get() = _publicPackages ?: extension?.publicPackages ?: emptySet()
60+
set(value) { _publicPackages = value }
61+
62+
private var _publicMarkers: Set<String>? = null
63+
@get:Input
64+
var publicMarkers: Set<String>
65+
get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet()
66+
set(value) { _publicMarkers = value}
67+
68+
private var _publicClasses: Set<String>? = null
69+
@get:Input
70+
var publicClasses: Set<String>
71+
get() = _publicClasses ?: extension?.publicClasses ?: emptySet()
72+
set(value) { _publicClasses = value }
73+
5674
@get:Internal
5775
internal val projectName = project.name
5876

@@ -79,8 +97,9 @@ open class KotlinApiBuildTask @Inject constructor(
7997

8098

8199
val filteredSignatures = signatures
100+
.retainExplicitlyIncludedIfDeclared(publicPackages, publicClasses, publicMarkers)
82101
.filterOutNonPublic(ignoredPackages, ignoredClasses)
83-
.filterOutAnnotated(nonPublicMarkers.map { it.replace(".", "/") }.toSet())
102+
.filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet())
84103

85104
outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer ->
86105
filteredSignatures

0 commit comments

Comments
 (0)