Skip to content

Commit

Permalink
The big refactor — step 1: prep work (#192)
Browse files Browse the repository at this point in the history
* Cleanup code, remove compiler warnings

* Add API validation to check task

1. Running binary-compatibility-validator on code
2. Flagging public data classes and failing build
3. Running Poko to autogen equals/hashcode/toString

* Remove all public data classes, add API dumps
  • Loading branch information
rock3r authored Oct 20, 2023
1 parent af53235 commit 0101ed3
Show file tree
Hide file tree
Showing 52 changed files with 5,119 additions and 155 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
name: Publish artifacts in Space

on:
release:
types: [ published ]
push:
branches: [ main ]

jobs:
publish-core:
name: Publish Jewel Core
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions .idea/runConfigurations/Run_checks__IJ_23_2_.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions .idea/runConfigurations/Update_API_signatures.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/IdeaConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ private var warned = AtomicBoolean(false)

fun Project.supportedIJVersion(): SupportedIJVersion {
val prop = kotlin.runCatching {
localProperty("supported.ij.version")
?: rootProject.property("supported.ij.version")?.toString()
rootProject.findProperty("supported.ij.version")?.toString() ?:
localProperty("supported.ij.version")
}.getOrNull()

if (prop == null) {
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,
)
29 changes: 29 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,29 @@
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")
}

poko {
pokoAnnotation.set("org.jetbrains.jewel.GenerateDataFunctions")
}

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

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

import io.gitlab.arturbosch.detekt.Detekt
import org.gradle.api.attributes.Usage
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask

plugins {
Expand Down Expand Up @@ -48,7 +49,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 All @@ -69,6 +70,11 @@ tasks {
}
}
}

withType<FormatTask> {
exclude { it.file.absolutePath.contains("build/generated") }
}

withType<LintTask> {
exclude { it.file.absolutePath.contains("build/generated") }

Expand Down
Loading

0 comments on commit 0101ed3

Please sign in to comment.