Skip to content

Commit

Permalink
Add API validation to check task
Browse files Browse the repository at this point in the history
1. Running binary-compatibility-validator on code
2. Flagging public data classes and failing build
3. Running Poko to autogen equals/hashcode/toString
  • Loading branch information
rock3r committed Oct 19, 2023
1 parent 66204bb commit 183d2ea
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 24 deletions.
11 changes: 7 additions & 4 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.kotlinx.serialization)
}

gradlePlugin {
Expand All @@ -27,13 +27,16 @@ kotlin {
}

dependencies {
implementation(libs.kotlin.gradlePlugin)
implementation(libs.kotlinter.gradlePlugin)
implementation(libs.detekt.gradlePlugin)
implementation(libs.kotlinSarif)
implementation(libs.dokka.gradlePlugin)
implementation(libs.kotlin.gradlePlugin)
implementation(libs.kotlinSarif)
implementation(libs.kotlinpoet)
implementation(libs.kotlinter.gradlePlugin)
implementation(libs.kotlinx.binaryCompatValidator.gradlePlugin)
implementation(libs.kotlinx.serialization.json)
implementation(libs.poko.gradlePlugin)

// Enables using type-safe accessors to reference plugins from the [plugins] block defined in version catalogs.
// Context: https://github.com/gradle/gradle/issues/15383#issuecomment-779893192
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
Expand Down
6 changes: 0 additions & 6 deletions buildSrc/src/main/kotlin/MergeSarifTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ import io.github.detekt.sarif4k.Version
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import org.gradle.api.file.FileTree
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.IgnoreEmptyDirectories
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction

Expand Down
97 changes: 97 additions & 0 deletions buildSrc/src/main/kotlin/ValidatePublicApiTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import org.gradle.api.GradleException
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.util.Stack

@CacheableTask
open class ValidatePublicApiTask : SourceTask() {

init {
group = "verification"
}

private val classFqnRegex = "public (?:\\w+ )*class (\\S+)\\b".toRegex()

@Suppress("ConvertToStringTemplate") // The odd concatenation is needed because of $; escapes get confused
private val copyMethodRegex = ("public static synthetic fun copy(-\\w+)" + "\\$" + "default\\b").toRegex()

@TaskAction
fun validatePublicApi() {
logger.info("Validating ${source.files.size} API file(s)...")

val violations = mutableMapOf<File, Set<String>>()
inputs.files.forEach { apiFile ->
logger.lifecycle("Validating public API from file ${apiFile.path}")

apiFile.useLines { lines ->
val actualDataClasses = findDataClasses(lines)

if (actualDataClasses.isNotEmpty()) {
violations[apiFile] = actualDataClasses
}
}
}

if (violations.isNotEmpty()) {
val message = buildString {
appendLine("Data classes found in public API.")
appendLine()

for ((file, dataClasses) in violations.entries) {
appendLine("In file ${file.path}:")
for (dataClass in dataClasses) {
appendLine(" * ${dataClass.replace("/", ".")}")
}
appendLine()
}
}

throw GradleException(message)
} else {
logger.lifecycle("No public API violations found.")
}
}

private fun findDataClasses(lines: Sequence<String>): Set<String> {
val currentClassStack = Stack<String>()
val dataClasses = mutableMapOf<String, DataClassInfo>()

for (line in lines) {
if (line.isBlank()) continue

val matchResult = classFqnRegex.find(line)
if (matchResult != null) {
val classFqn = matchResult.groupValues[1]
currentClassStack.push(classFqn)
continue
}

if (line.contains("}")) {
currentClassStack.pop()
continue
}

val fqn = currentClassStack.peek()
if (copyMethodRegex.find(line) != null) {
val info = dataClasses.getOrPut(fqn) { DataClassInfo(fqn) }
info.hasCopyMethod = true
} else if (line.contains("public static final synthetic fun box-impl")) {
val info = dataClasses.getOrPut(fqn) { DataClassInfo(fqn) }
info.isLikelyValueClass = true
}
}

val actualDataClasses = dataClasses.filterValues { it.hasCopyMethod && !it.isLikelyValueClass }
.keys
return actualDataClasses
}
}

@Suppress("DataClassShouldBeImmutable") // Only used in a loop, saves memory and is faster
private data class DataClassInfo(
val fqn: String,
var hasCopyMethod: Boolean = false,
var isLikelyValueClass: Boolean = false,
)
24 changes: 24 additions & 0 deletions buildSrc/src/main/kotlin/jewel-check-public-api.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id("org.jetbrains.kotlinx.binary-compatibility-validator")
id("dev.drewhamilton.poko")
}

apiValidation {
/**
* Set of annotations that exclude API from being public. Typically, it is
* all kinds of `@InternalApi` annotations that mark effectively private
* API that cannot be actually private for technical reasons.
*/
nonPublicMarkers.add("org.jetbrains.jewel.InternalJewelApi")
}

tasks {
val validatePublicApi = register<ValidatePublicApiTask>("validatePublicApi") {
include { it.file.extension == "api" }
source(project.fileTree("api"))
}

named("check") {
dependsOn(validatePublicApi)
}
}
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/jewel.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ detekt {
buildUponDefaultConfig = true
}

val sarif by configurations.creating {
val sarif: Configuration by configurations.creating {
isCanBeConsumed = true
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named("sarif"))
Expand Down
3 changes: 2 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import org.jetbrains.compose.ComposeBuildConfig
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.kotlinx.serialization)
}

private val composeVersion get() = ComposeBuildConfig.composeVersion
Expand Down
2 changes: 1 addition & 1 deletion decorated-window/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import org.jetbrains.compose.ComposeBuildConfig
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
alias(libs.plugins.kotlinSerialization)
}

private val composeVersion get() = ComposeBuildConfig.composeVersion
Expand Down
25 changes: 15 additions & 10 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
composeDesktop = "1.5.2"
coroutines = "1.7.3"
detekt = "1.23.1"
dokka = "1.8.20"
idea232 = "232.8660.185"
idea233 = "233.9802.14-EAP-SNAPSHOT"
ideaGradlePlugin = "1.15.0"
javaSarif = "2.0"
kotlinSarif = "0.4.0"
jna = "5.13.0"
kotlin = "1.8.21"
dokka = "1.8.20"
kotlinSarif = "0.4.0"
kotlinpoet = "1.14.2"
kotlinterGradlePlugin = "3.16.0"
kotlinxSerialization = "1.5.1"
kotlinpoet = "1.14.2"
kotlinxBinaryCompat = "0.13.2"
poko = "0.13.1"
semVer = "1.2.0"
simpleXml = "2.7.1"
jna = "5.13.0"

[libraries]
javaSarif = { module = "com.contrastsecurity:java-sarif", version.ref = "javaSarif" }
Expand All @@ -36,10 +38,12 @@ jna-core = { module = "net.java.dev.jna:jna", version.ref = "jna" }

# Plugin libraries for build-logic's convention plugins to use to resolve the types/tasks coming from these plugins
detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinter-gradlePlugin = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "kotlinterGradlePlugin" }
dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
kotlinter-gradlePlugin = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "kotlinterGradlePlugin" }
kotlinx-binaryCompatValidator-gradlePlugin = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "kotlinxBinaryCompat" }
poko-gradlePlugin = { module = "dev.drewhamilton.poko:poko-gradle-plugin", version.ref = "poko" }

[bundles]
idea232 = ["ij-platform-ide-core-232", "ij-platform-ide-impl-232", "ij-platform-core-ui-232"]
Expand All @@ -48,9 +52,10 @@ idea233 = ["ij-platform-ide-core-233", "ij-platform-ide-impl-233", "ij-platform-
[plugins]
composeDesktop = { id = "org.jetbrains.compose", version.ref = "composeDesktop" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
ideaGradlePlugin = { id = "org.jetbrains.intellij", version.ref = "ideaGradlePlugin" }
kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinterGradlePlugin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
kotlinx-binaryCompatValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlinxBinaryCompat" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinterGradlePlugin" }
poko = { id = "dev.drewhamilton.poko", version.ref = "poko" }
3 changes: 2 additions & 1 deletion ide-laf-bridge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SupportedIJVersion.*
plugins {
jewel
`jewel-ij-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
}

Expand All @@ -26,4 +27,4 @@ dependencies {
testImplementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
}
}
1 change: 1 addition & 0 deletions int-ui/int-ui-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
`intellij-theme-generator`
}
Expand Down
1 change: 1 addition & 0 deletions int-ui/int-ui-decorated-window/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
}

Expand Down
1 change: 1 addition & 0 deletions int-ui/int-ui-standalone/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
}

Expand Down

0 comments on commit 183d2ea

Please sign in to comment.