diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 345c3aaf5a..11c2ce89b4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,6 +1,6 @@ plugins { `kotlin-dsl` - alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.kotlinx.serialization) } gradlePlugin { @@ -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)) diff --git a/buildSrc/src/main/kotlin/MergeSarifTask.kt b/buildSrc/src/main/kotlin/MergeSarifTask.kt index d0d1f9778a..54e7fed105 100644 --- a/buildSrc/src/main/kotlin/MergeSarifTask.kt +++ b/buildSrc/src/main/kotlin/MergeSarifTask.kt @@ -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 diff --git a/buildSrc/src/main/kotlin/ValidatePublicApiTask.kt b/buildSrc/src/main/kotlin/ValidatePublicApiTask.kt new file mode 100644 index 0000000000..722d119745 --- /dev/null +++ b/buildSrc/src/main/kotlin/ValidatePublicApiTask.kt @@ -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>() + 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): Set { + val currentClassStack = Stack() + val dataClasses = mutableMapOf() + + 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, +) diff --git a/buildSrc/src/main/kotlin/jewel-check-public-api.gradle.kts b/buildSrc/src/main/kotlin/jewel-check-public-api.gradle.kts new file mode 100644 index 0000000000..37c69ea805 --- /dev/null +++ b/buildSrc/src/main/kotlin/jewel-check-public-api.gradle.kts @@ -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("validatePublicApi") { + include { it.file.extension == "api" } + source(project.fileTree("api")) + } + + named("check") { + dependsOn(validatePublicApi) + } +} diff --git a/buildSrc/src/main/kotlin/jewel.gradle.kts b/buildSrc/src/main/kotlin/jewel.gradle.kts index 9760b645f0..f5f701aa8c 100644 --- a/buildSrc/src/main/kotlin/jewel.gradle.kts +++ b/buildSrc/src/main/kotlin/jewel.gradle.kts @@ -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")) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f0a74b272e..8220609559 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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 diff --git a/decorated-window/build.gradle.kts b/decorated-window/build.gradle.kts index 577c600d3f..2e7691407d 100644 --- a/decorated-window/build.gradle.kts +++ b/decorated-window/build.gradle.kts @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b98c9ff9f8..402a34ee2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } @@ -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"] @@ -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" } diff --git a/ide-laf-bridge/build.gradle.kts b/ide-laf-bridge/build.gradle.kts index 186521f935..c504420fbe 100644 --- a/ide-laf-bridge/build.gradle.kts +++ b/ide-laf-bridge/build.gradle.kts @@ -3,6 +3,7 @@ import SupportedIJVersion.* plugins { jewel `jewel-ij-publish` + `jewel-check-public-api` alias(libs.plugins.composeDesktop) } @@ -26,4 +27,4 @@ dependencies { testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } -} \ No newline at end of file +} diff --git a/int-ui/int-ui-core/build.gradle.kts b/int-ui/int-ui-core/build.gradle.kts index 53a0105a9a..f383a3c83d 100644 --- a/int-ui/int-ui-core/build.gradle.kts +++ b/int-ui/int-ui-core/build.gradle.kts @@ -3,6 +3,7 @@ plugins { jewel `jewel-publish` + `jewel-check-public-api` alias(libs.plugins.composeDesktop) `intellij-theme-generator` } diff --git a/int-ui/int-ui-decorated-window/build.gradle.kts b/int-ui/int-ui-decorated-window/build.gradle.kts index 3f6e04f83c..0cca652add 100644 --- a/int-ui/int-ui-decorated-window/build.gradle.kts +++ b/int-ui/int-ui-decorated-window/build.gradle.kts @@ -1,6 +1,7 @@ plugins { jewel `jewel-publish` + `jewel-check-public-api` alias(libs.plugins.composeDesktop) } diff --git a/int-ui/int-ui-standalone/build.gradle.kts b/int-ui/int-ui-standalone/build.gradle.kts index fd6cb3fc4c..831b1ecaac 100644 --- a/int-ui/int-ui-standalone/build.gradle.kts +++ b/int-ui/int-ui-standalone/build.gradle.kts @@ -1,6 +1,7 @@ plugins { jewel `jewel-publish` + `jewel-check-public-api` alias(libs.plugins.composeDesktop) }