Skip to content

Commit

Permalink
Support for explicit public API markers (#116)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
KirpichenkovPavel committed Mar 3, 2023
1 parent c595e65 commit 5d08f51
Show file tree
Hide file tree
Showing 13 changed files with 357 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2016-2022 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 kotlinx.validation.test

import kotlinx.validation.api.*
import kotlinx.validation.api.buildGradleKts
import kotlinx.validation.api.kotlin
import kotlinx.validation.api.resolve
import kotlinx.validation.api.test
import org.junit.Test

class MixedMarkersTest : BaseKotlinGradleTest() {

@Test
fun testMixedMarkers() {
val runner = test {
buildGradleKts {
resolve("examples/gradle/base/withPlugin.gradle.kts")
resolve("examples/gradle/configuration/publicMarkers/mixedMarkers.gradle.kts")
}

kotlin("MixedAnnotations.kt") {
resolve("examples/classes/MixedAnnotations.kt")
}

apiFile(projectName = rootProjectDir.name) {
resolve("examples/classes/MixedAnnotations.dump")
}

runner {
arguments.add(":apiCheck")
}
}

runner.withDebug(true).build().apply {
assertTaskSuccess(":apiCheck")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2016-2022 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 kotlinx.validation.test

import kotlinx.validation.api.*
import kotlinx.validation.api.buildGradleKts
import kotlinx.validation.api.kotlin
import kotlinx.validation.api.resolve
import kotlinx.validation.api.test
import org.junit.Test

class PublicMarkersTest : BaseKotlinGradleTest() {

@Test
fun testPublicMarkers() {
val runner = test {
buildGradleKts {
resolve("examples/gradle/base/withPlugin.gradle.kts")
resolve("examples/gradle/configuration/publicMarkers/markers.gradle.kts")
}

kotlin("ClassWithPublicMarkers.kt") {
resolve("examples/classes/ClassWithPublicMarkers.kt")
}

kotlin("ClassInPublicPackage.kt") {
resolve("examples/classes/ClassInPublicPackage.kt")
}

apiFile(projectName = rootProjectDir.name) {
resolve("examples/classes/ClassWithPublicMarkers.dump")
}

runner {
arguments.add(":apiCheck")
}
}

runner.withDebug(true).build().apply {
assertTaskSuccess(":apiCheck")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright 2016-2022 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 foo.api

class ClassInPublicPackage {
class Inner
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public final class foo/ClassWithPublicMarkers {
public final fun getBar1 ()I
public final fun getBar2 ()I
public final fun setBar1 (I)V
public final fun setBar2 (I)V
}

public final class foo/ClassWithPublicMarkers$MarkedClass {
public fun <init> ()V
public final fun getBar1 ()I
}

public abstract interface annotation class foo/PublicClass : java/lang/annotation/Annotation {
}

public final class foo/api/ClassInPublicPackage {
public fun <init> ()V
}

public final class foo/api/ClassInPublicPackage$Inner {
public fun <init> ()V
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2016-2022 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 foo

@Target(AnnotationTarget.CLASS)
annotation class PublicClass

@Target(AnnotationTarget.FIELD)
annotation class PublicField

@Target(AnnotationTarget.PROPERTY)
annotation class PublicProperty

public class ClassWithPublicMarkers {
@PublicField
var bar1 = 42

@PublicProperty
var bar2 = 42

@PublicClass
class MarkedClass {
val bar1 = 41
}

var notMarkedPublic = 42

class NotMarkedClass
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public final class mixed/MarkedPublicWithPrivateMembers {
public fun <init> ()V
public final fun otherFun ()V
}

49 changes: 49 additions & 0 deletions src/functionalTest/resources/examples/classes/MixedAnnotations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2016-2022 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 mixed

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
annotation class PublicApi

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
annotation class PrivateApi

@PublicApi
class MarkedPublicWithPrivateMembers {
@PrivateApi
var private1 = 42

@field:PrivateApi
var private2 = 15

@PrivateApi
fun privateFun() = Unit

@PublicApi
@PrivateApi
fun privateFun2() = Unit

fun otherFun() = Unit
}

// Member annotations should be ignored in explicitly private classes
@PrivateApi
class MarkedPrivateWithPublicMembers {
@PublicApi
var public1 = 42

@field:PublicApi
var public2 = 15

@PublicApi
fun publicFun() = Unit

fun otherFun() = Unit
}

@PrivateApi
@PublicApi
class PublicAndPrivateFilteredOut
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright 2016-2022 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("foo.PublicClass")
publicMarkers.add("foo.PublicField")
publicMarkers.add("foo.PublicProperty")

publicPackages.add("foo.api")
publicClasses.add("foo.PublicClass")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2016-2022 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("mixed.PrivateApi")
publicMarkers.add("mixed.PublicApi")
}
30 changes: 30 additions & 0 deletions src/main/kotlin/ApiValidationExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,34 @@ open class ApiValidationExtension {
* Example of such a class could be `com.package.android.BuildConfig`.
*/
public var ignoredClasses: MutableSet<String> = HashSet()

/**
* Fully qualified names of annotations that can be used to explicitly mark public declarations.
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
* all declarations not covered by any of them will be considered non-public.
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
*/
public var publicMarkers: MutableSet<String> = HashSet()

/**
* Fully qualified package names that contain public declarations.
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
* all declarations not covered by any of them will be considered non-public.
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
*/
public var publicPackages: MutableSet<String> = HashSet()

/**
* Fully qualified names of public classes.
* If at least one of [publicMarkers], [publicPackages] or [publicClasses] is defined,
* all declarations not covered by any of them will be considered non-public.
* [ignoredPackages], [ignoredClasses] and [nonPublicMarkers] can be used for additional filtering.
*/
public var publicClasses: MutableSet<String> = HashSet()

/**
* Non-default Gradle SourceSet names that should be validated.
* By default, only the `main` source set is checked.
*/
public var additionalSourceSets: MutableSet<String> = HashSet()
}
18 changes: 9 additions & 9 deletions src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,7 @@ class BinaryCompatibilityValidatorPlugin : Plugin<Project> {
project: Project,
extension: ApiValidationExtension
) = configurePlugin("kotlin", project, extension) {
project.sourceSets.all { sourceSet ->
if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) {
return@all
}
project.configureApiTasks(sourceSet, extension, TargetConfig(project))
}
project.configureApiTasks(extension, TargetConfig(project))
}
}

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

private fun Project.configureApiTasks(
sourceSet: SourceSet,
extension: ApiValidationExtension,
targetConfig: TargetConfig = TargetConfig(this),
) {
val projectName = project.name
val apiBuildDir = targetConfig.apiDir.map { buildDir.resolve(it) }
val sourceSetsOutputsProvider = project.provider {
sourceSets
.filter { it.name == SourceSet.MAIN_SOURCE_SET_NAME || it.name in extension.additionalSourceSets }
.map { it.output.classesDirs }
}

val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build")) {
isEnabled = apiCheckEnabled(projectName, extension)
// 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
description =
"Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually"
inputClassesDirs = files(provider<Any> { if (isEnabled) sourceSet.output.classesDirs else emptyList<Any>() })
inputDependencies = files(provider<Any> { if (isEnabled) sourceSet.output.classesDirs else emptyList<Any>() })
inputClassesDirs = files(provider<Any> { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList<Any>() })
inputDependencies = files(provider<Any> { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList<Any>() })
outputApiDir = apiBuildDir.get()
}

Expand Down
21 changes: 20 additions & 1 deletion src/main/kotlin/KotlinApiBuildTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ open class KotlinApiBuildTask @Inject constructor(
get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet()
set(value) { _ignoredClasses = value }

private var _publicPackages: Set<String>? = null
@get:Input
var publicPackages: Set<String>
get() = _publicPackages ?: extension?.publicPackages ?: emptySet()
set(value) { _publicPackages = value }

private var _publicMarkers: Set<String>? = null
@get:Input
var publicMarkers: Set<String>
get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet()
set(value) { _publicMarkers = value}

private var _publicClasses: Set<String>? = null
@get:Input
var publicClasses: Set<String>
get() = _publicClasses ?: extension?.publicClasses ?: emptySet()
set(value) { _publicClasses = value }

@get:Internal
internal val projectName = project.name

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


val filteredSignatures = signatures
.retainExplicitlyIncludedIfDeclared(publicPackages, publicClasses, publicMarkers)
.filterOutNonPublic(ignoredPackages, ignoredClasses)
.filterOutAnnotated(nonPublicMarkers.map { it.replace(".", "/") }.toSet())
.filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet())

outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer ->
filteredSignatures
Expand Down
Loading

0 comments on commit 5d08f51

Please sign in to comment.