From 789d91c55c957ad50e7aca11eec3fee639bef908 Mon Sep 17 00:00:00 2001 From: Yang Date: Tue, 6 May 2025 11:29:11 +1000 Subject: [PATCH 1/3] Cocoon compiler plugin skeleton. --- .../cocoon-compiler-plugin/build.gradle.kts | 54 +++++++++++++++++ .../cocoon-compiler-plugin/gradle.properties | 1 + .../compiler/CocoonCommandLineProcessor.kt | 39 +++++++++++++ .../compiler/CocoonCompilerPluginRegistrar.kt | 37 ++++++++++++ .../compiler/CocoonFunctionTransformer.kt | 16 +++++ .../compiler/CocoonIrGenerationExtension.kt | 35 +++++++++++ .../reactivecircus/cocoon/compiler/utils.kt | 11 ++++ ...otlin.compiler.plugin.CommandLineProcessor | 1 + ...in.compiler.plugin.CompilerPluginRegistrar | 1 + .../cocoon-gradle-plugin/build.gradle.kts | 58 +++++++++++++++++++ .../cocoon/gradle/CocoonExtension.kt | 10 ++++ .../cocoon/gradle/CocoonPlugin.kt | 45 ++++++++++++++ build-logic/settings.gradle.kts | 2 + gradle/libs.versions.toml | 2 + 14 files changed, 312 insertions(+) create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/gradle.properties create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/utils.kt create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor create mode 100644 build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar create mode 100644 build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts create mode 100644 build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt create mode 100644 build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt diff --git a/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts b/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts new file mode 100644 index 00000000..fcab9465 --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts @@ -0,0 +1,54 @@ +import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) +} + +group = "io.github.reactivecircus.cocoon" +version = "0.1.0" + +kotlin { + explicitApi() +} + +tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") + } +} + +tasks.withType().configureEach { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() +} + +detekt { + source.from(files("src/")) + config.from(files("$rootDir/../detekt.yml")) + buildUponDefaultConfig = true + allRules = true + parallel = true +} + +tasks.withType().configureEach { + jvmTarget = JvmTarget.JVM_11.target + reports { + xml.required.set(false) + txt.required.set(false) + sarif.required.set(false) + md.required.set(false) + } +} + +dependencies { + // enable Ktlint formatting + add("detektPlugins", libs.plugin.detektFormatting) + + compileOnly(libs.kotlin.compiler) + compileOnly(libs.kotlin.stblib) +} diff --git a/build-logic/cocoon/cocoon-compiler-plugin/gradle.properties b/build-logic/cocoon/cocoon-compiler-plugin/gradle.properties new file mode 100644 index 00000000..0d6aa7b6 --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/gradle.properties @@ -0,0 +1 @@ +kotlin.stdlib.default.dependency=false diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt new file mode 100644 index 00000000..0d0ff632 --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt @@ -0,0 +1,39 @@ +package io.github.reactivecircus.cocoon.compiler + +import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfigurationKey + +public class CocoonCommandLineProcessor : CommandLineProcessor { + + override val pluginId: String = "io.github.reactivecircus.cocoon.compiler" + + @Suppress("MaxLineLength") + override val pluginOptions: Collection = listOf( + CliOption( + optionName = CompilerOptions.Annotation.toString(), + valueDescription = "Fully qualified annotation class name", + description = "The fully qualified name of the annotation to be used for marking functions for transformation.", + ), + CliOption( + optionName = CompilerOptions.WrappingFunction.toString(), + valueDescription = "Fully qualified name of the higher-order function to be used for wrapping the transformed function's body.", + description = "The fully qualified name of the function to be used for wrapping the transformed function.", + ), + ) + + override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { + when (option.optionName) { + CompilerOptions.Annotation.toString() -> configuration.put(CompilerOptions.Annotation, value) + CompilerOptions.WrappingFunction.toString() -> configuration.put(CompilerOptions.WrappingFunction, value) + else -> throw IllegalArgumentException("Unknown plugin option: ${option.optionName}") + } + } + + internal object CompilerOptions { + val Annotation = CompilerConfigurationKey("annotation") + val WrappingFunction = CompilerConfigurationKey("wrappingFunction") + } +} diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt new file mode 100644 index 00000000..58ce2669 --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt @@ -0,0 +1,37 @@ +package io.github.reactivecircus.cocoon.compiler + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration + +public class CocoonCompilerPluginRegistrar : CompilerPluginRegistrar() { + + override val supportsK2: Boolean = true + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + val annotationString = requireNotNull( + configuration.get(CocoonCommandLineProcessor.CompilerOptions.Annotation) + ) + val annotationClassId = annotationString.toClassId() + + val wrappingFunctionString = requireNotNull( + configuration.get(CocoonCommandLineProcessor.CompilerOptions.WrappingFunction) + ) + val wrappingFunctionCallableId = wrappingFunctionString.toCallableId() + + val messageCollector = configuration.get( + CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, + MessageCollector.NONE + ) + + IrGenerationExtension.registerExtension( + extension = CocoonIrGenerationExtension( + annotationName = annotationClassId, + wrappingFunctionName = wrappingFunctionCallableId, + messageCollector = messageCollector, + ) + ) + } +} diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt new file mode 100644 index 00000000..abe9a485 --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt @@ -0,0 +1,16 @@ +package io.github.reactivecircus.cocoon.compiler + +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId + +internal class CocoonFunctionTransformer( + private val pluginContext: IrPluginContext, + private val messageCollector: MessageCollector, + private val annotationName: ClassId, + private val wrappingFunctionName: CallableId, +) : IrElementTransformerVoidWithContext() { + +} diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt new file mode 100644 index 00000000..ddfda24e --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt @@ -0,0 +1,35 @@ +package io.github.reactivecircus.cocoon.compiler + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId + +internal class CocoonIrGenerationExtension( + private val annotationName: ClassId, + private val wrappingFunctionName: CallableId, + private val messageCollector: MessageCollector, +) : IrGenerationExtension { + @OptIn(UnsafeDuringIrConstructionAPI::class) + override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { + if (pluginContext.referenceClass(annotationName) == null) { + messageCollector.report(CompilerMessageSeverity.ERROR, "Could not find annotation class <$annotationName>.") + return + } + if (pluginContext.referenceFunctions(wrappingFunctionName).isEmpty()) { + messageCollector.report( + CompilerMessageSeverity.ERROR, + "Could not find wrapping function <$wrappingFunctionName>.", + ) + return + } + moduleFragment.transform( + CocoonFunctionTransformer(pluginContext, messageCollector, annotationName, wrappingFunctionName), + null, + ) + } +} diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/utils.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/utils.kt new file mode 100644 index 00000000..cb1b7eb1 --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/utils.kt @@ -0,0 +1,11 @@ +package io.github.reactivecircus.cocoon.compiler + +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +internal fun String.toClassId(): ClassId = + FqName(this).run { ClassId(parent(), shortName()) } + +internal fun String.toCallableId(): CallableId = + FqName(this).run { CallableId(parent(), shortName()) } diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor new file mode 100644 index 00000000..0b1f1d0d --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor @@ -0,0 +1 @@ +io.github.reactivecircus.cocoon.compiler.CocoonCommandLineProcessor diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar new file mode 100644 index 00000000..5679cf9c --- /dev/null +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar @@ -0,0 +1 @@ +io.github.reactivecircus.cocoon.compiler.CocoonCompilerPluginRegistrar diff --git a/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts b/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts new file mode 100644 index 00000000..7f413b41 --- /dev/null +++ b/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts @@ -0,0 +1,58 @@ +import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `kotlin-dsl` + alias(libs.plugins.detekt) +} + +gradlePlugin { + plugins { + register("cocoon") { + id = "io.github.reactivecircus.cocoon" + implementationClass = "io.github.reactivecircus.cocoon.gradle.CocoonPlugin" + } + } +} + +kotlin { + explicitApi() +} + +tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} + +tasks.withType().configureEach { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() +} + +detekt { + source.from(files("src/")) + config.from(files("$rootDir/../detekt.yml")) + buildUponDefaultConfig = true + allRules = true + parallel = true +} + +tasks.withType().configureEach { + jvmTarget = JvmTarget.JVM_11.target + reports { + xml.required.set(false) + txt.required.set(false) + sarif.required.set(false) + md.required.set(false) + } +} + +dependencies { + // enable Ktlint formatting + add("detektPlugins", libs.plugin.detektFormatting) + + compileOnly(libs.plugin.kotlin) +} diff --git a/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt new file mode 100644 index 00000000..3883e5d3 --- /dev/null +++ b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt @@ -0,0 +1,10 @@ +package io.github.reactivecircus.cocoon.gradle + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.property + +public abstract class CocoonExtension internal constructor(objects: ObjectFactory) { + public val annotation: Property = objects.property() + public val wrappingFunction: Property = objects.property() +} diff --git a/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt new file mode 100644 index 00000000..fe693e5c --- /dev/null +++ b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt @@ -0,0 +1,45 @@ +package io.github.reactivecircus.cocoon.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption + +public class CocoonPlugin : KotlinCompilerPluginSupportPlugin { + + override fun apply(target: Project) { + target.extensions.create("cocoon", CocoonExtension::class.java) + } + + override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { + val project = kotlinCompilation.target.project + val extension = project.extensions.getByType() + return project.provider { + listOf( + SubpluginOption( + key = "annotation", + value = extension.annotation.get(), + ), + SubpluginOption( + key = "wrappingFunction", + value = extension.wrappingFunction.get(), + ), + ) + } + } + + override fun getCompilerPluginId(): String = "io.github.reactivecircus.cocoon.compiler" + + override fun getPluginArtifact(): SubpluginArtifact { + return SubpluginArtifact( + groupId = "io.github.reactivecircus.cocoon", + artifactId = "cocoon-compiler-plugin", + version = "0.1.0", + ) + } + + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 82196cb5..7b4dc9e2 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -53,3 +53,5 @@ plugins { rootProject.name = "build-logic" include(":kstreamlined-gradle-plugin") +include(":cocoon:cocoon-compiler-plugin") +include(":cocoon:cocoon-gradle-plugin") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f102e011..c754e6cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -84,6 +84,8 @@ plugin-playPublisher = { module = "com.github.triplet.gradle:play-publisher", ve plugin-skie = { module = "co.touchlab.skie:co.touchlab.skie.gradle.plugin", version.ref = "skie"} plugin-sqldelight = { module = "app.cash.sqldelight:app.cash.sqldelight.gradle.plugin", version.ref = "sqldelight"} plugin-baselineprofile = { module = "androidx.baselineprofile:androidx.baselineprofile.gradle.plugin", version.ref = "androidx-benchmark"} +kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin"} +kotlin-stblib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin"} leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } From fc25a1d3ae39e08a7ff9c95ceba33613776a5f16 Mon Sep 17 00:00:00 2001 From: Yang Date: Fri, 9 May 2025 17:08:09 +1000 Subject: [PATCH 2/3] Basic IR transformer. Enable in `designsystem` module. --- .../foundation/designsystem/build.gradle.kts | 6 ++ .../designsystem/component/Button.kt | 18 ++-- .../preview/PreviewKStreamlined.kt | 22 +++++ .../compiler/CocoonFunctionTransformer.kt | 95 ++++++++++++++++++- .../compiler/CocoonIrGenerationExtension.kt | 2 - detekt.yml | 1 + 6 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt diff --git a/android/foundation/designsystem/build.gradle.kts b/android/foundation/designsystem/build.gradle.kts index 78db1935..b8a81f69 100644 --- a/android/foundation/designsystem/build.gradle.kts +++ b/android/foundation/designsystem/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("kstreamlined.android.library") id("kstreamlined.android.screenshot-test") + id("io.github.reactivecircus.cocoon") id("kstreamlined.compose") } @@ -9,6 +10,11 @@ android { androidResources.enable = true } +cocoon { + annotation.set("io.github.reactivecircus.kstreamlined.android.foundation.designsystem.preview.PreviewKStreamlined") + wrappingFunction.set("io.github.reactivecircus.kstreamlined.android.foundation.designsystem.preview.KSThemeWithSurface") +} + dependencies { implementation(libs.androidx.compose.materialIcons) implementation(libs.androidx.compose.material3) diff --git a/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/component/Button.kt b/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/component/Button.kt index 333425d4..282aafd4 100644 --- a/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/component/Button.kt +++ b/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/component/Button.kt @@ -11,9 +11,9 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import io.github.reactivecircus.kstreamlined.android.foundation.designsystem.foundation.KSTheme +import io.github.reactivecircus.kstreamlined.android.foundation.designsystem.preview.PreviewKStreamlined @Composable public fun Button( @@ -50,15 +50,11 @@ public fun Button( } @Composable -@PreviewLightDark +@PreviewKStreamlined private fun PreviewButton() { - KSTheme { - Surface { - Button( - text = "Button", - onClick = {}, - modifier = Modifier.padding(8.dp), - ) - } - } + Button( + text = "Button", + onClick = {}, + modifier = Modifier.padding(8.dp), + ) } diff --git a/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt b/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt new file mode 100644 index 00000000..c91a9bf3 --- /dev/null +++ b/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt @@ -0,0 +1,22 @@ +package io.github.reactivecircus.kstreamlined.android.foundation.designsystem.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import io.github.reactivecircus.kstreamlined.android.foundation.designsystem.component.Surface +import io.github.reactivecircus.kstreamlined.android.foundation.designsystem.foundation.KSTheme + +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION) +@PreviewLightDark +public annotation class PreviewKStreamlined + +@Composable +public fun KSThemeWithSurface( + content: @Composable () -> Unit +) { + KSTheme { + Surface { + content() + } + } +} diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt index abe9a485..42c7ec0a 100644 --- a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt @@ -2,15 +2,106 @@ package io.github.reactivecircus.cocoon.compiler import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.builders.declarations.buildFun +import org.jetbrains.kotlin.ir.builders.irBlock +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.createBlockBody +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.dump +import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.statements import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.SpecialNames internal class CocoonFunctionTransformer( private val pluginContext: IrPluginContext, private val messageCollector: MessageCollector, - private val annotationName: ClassId, - private val wrappingFunctionName: CallableId, + private val annotation: ClassId, + private val wrappingFunction: CallableId, ) : IrElementTransformerVoidWithContext() { + @OptIn(UnsafeDuringIrConstructionAPI::class) + override fun visitFunctionNew(declaration: IrFunction): IrStatement { + if (!declaration.hasAnnotation(annotation) || declaration.body == null) { + return super.visitFunctionNew(declaration) + } + + // TODO check for $composer and report error + + val originalBody = declaration.body!! + + declaration.body = pluginContext.irFactory.createBlockBody( + startOffset = originalBody.startOffset, + endOffset = originalBody.endOffset, + ).apply { + val wrappingFunction = pluginContext.referenceFunctions(wrappingFunction).single() + val irBuilder = DeclarationIrBuilder(pluginContext, declaration.symbol) + + // TODO move up and check early: + // - must have at least 1 param + // - last must be kotlin.Function0) + // - move to FIR? + val wrappingFunctionParameters = wrappingFunction.owner.parameters + + statements.add( + irBuilder.irBlock { + +irCall(wrappingFunction).apply { + val lambdaExpression = pluginContext.createLambdaIrFunctionExpression( + lambdaReturnType = wrappingFunctionParameters.last().type, + ) { + parent = declaration + body = pluginContext.irFactory.createBlockBody( + startOffset, + endOffset, + originalBody.statements, + ) + } + arguments[wrappingFunctionParameters.size - 1] = lambdaExpression + } + } + ) + } + + log("Transformed function IR: \n${declaration.dump()}") + + return super.visitFunctionNew(declaration) + } + + private fun IrPluginContext.createLambdaIrFunctionExpression( + lambdaReturnType: IrType, + block: IrSimpleFunction.() -> Unit = {}, + ): IrExpression { + val lambda = irFactory.buildFun { + name = SpecialNames.ANONYMOUS + origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA + visibility = DescriptorVisibilities.LOCAL + returnType = lambdaReturnType + }.apply(block) + + return IrFunctionExpressionImpl( + startOffset = UNDEFINED_OFFSET, + endOffset = UNDEFINED_OFFSET, + type = lambda.returnType, + function = lambda, + origin = IrStatementOrigin.LAMBDA, + ) + } + + private fun log(message: String) { + messageCollector.report(CompilerMessageSeverity.LOGGING, "Cocoon Compiler Plugin (IR) - $message") + } } diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt index ddfda24e..5a72a566 100644 --- a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonIrGenerationExtension.kt @@ -5,7 +5,6 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.declarations.IrModuleFragment -import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId @@ -14,7 +13,6 @@ internal class CocoonIrGenerationExtension( private val wrappingFunctionName: CallableId, private val messageCollector: MessageCollector, ) : IrGenerationExtension { - @OptIn(UnsafeDuringIrConstructionAPI::class) override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { if (pluginContext.referenceClass(annotationName) == null) { messageCollector.report(CompilerMessageSeverity.ERROR, "Could not find annotation class <$annotationName>.") diff --git a/detekt.yml b/detekt.yml index d5f82adf..f863bb02 100644 --- a/detekt.yml +++ b/detekt.yml @@ -46,3 +46,4 @@ style: ignoreAnnotated: - Preview - PreviewLightDark + - PreviewKStreamlined From e3f99481aa2ff02fdbfecb7c8950220cf2f034bc Mon Sep 17 00:00:00 2001 From: Yang Date: Mon, 22 Sep 2025 21:17:32 +1000 Subject: [PATCH 3/3] Update detekt config, remove `kotlin-dsl`. --- .../designsystem/preview/PreviewKStreamlined.kt | 2 +- .../cocoon/cocoon-compiler-plugin/build.gradle.kts | 6 ++---- .../cocoon/compiler/CocoonCommandLineProcessor.kt | 1 - .../cocoon/compiler/CocoonCompilerPluginRegistrar.kt | 9 ++++----- .../cocoon/compiler/CocoonFunctionTransformer.kt | 3 +-- build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts | 9 ++++----- .../reactivecircus/cocoon/gradle/CocoonExtension.kt | 5 ++--- .../github/reactivecircus/cocoon/gradle/CocoonPlugin.kt | 4 +--- 8 files changed, 15 insertions(+), 24 deletions(-) diff --git a/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt b/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt index c91a9bf3..da3ffdf3 100644 --- a/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt +++ b/android/foundation/designsystem/src/main/kotlin/io/github/reactivecircus/kstreamlined/android/foundation/designsystem/preview/PreviewKStreamlined.kt @@ -12,7 +12,7 @@ public annotation class PreviewKStreamlined @Composable public fun KSThemeWithSurface( - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { KSTheme { Surface { diff --git a/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts b/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts index fcab9465..ec242fdb 100644 --- a/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts +++ b/build-logic/cocoon/cocoon-compiler-plugin/build.gradle.kts @@ -1,4 +1,4 @@ -import io.gitlab.arturbosch.detekt.Detekt +import dev.detekt.gradle.Detekt import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -31,7 +31,6 @@ detekt { source.from(files("src/")) config.from(files("$rootDir/../detekt.yml")) buildUponDefaultConfig = true - allRules = true parallel = true } @@ -39,7 +38,6 @@ tasks.withType().configureEach { jvmTarget = JvmTarget.JVM_11.target reports { xml.required.set(false) - txt.required.set(false) sarif.required.set(false) md.required.set(false) } @@ -47,7 +45,7 @@ tasks.withType().configureEach { dependencies { // enable Ktlint formatting - add("detektPlugins", libs.plugin.detektFormatting) + detektPlugins(libs.plugin.detektKtlintWrapper) compileOnly(libs.kotlin.compiler) compileOnly(libs.kotlin.stblib) diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt index 0d0ff632..e1505888 100644 --- a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCommandLineProcessor.kt @@ -7,7 +7,6 @@ import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.CompilerConfigurationKey public class CocoonCommandLineProcessor : CommandLineProcessor { - override val pluginId: String = "io.github.reactivecircus.cocoon.compiler" @Suppress("MaxLineLength") diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt index 58ce2669..1e867ef1 100644 --- a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonCompilerPluginRegistrar.kt @@ -7,23 +7,22 @@ import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.config.CompilerConfiguration public class CocoonCompilerPluginRegistrar : CompilerPluginRegistrar() { - override val supportsK2: Boolean = true override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { val annotationString = requireNotNull( - configuration.get(CocoonCommandLineProcessor.CompilerOptions.Annotation) + configuration.get(CocoonCommandLineProcessor.CompilerOptions.Annotation), ) val annotationClassId = annotationString.toClassId() val wrappingFunctionString = requireNotNull( - configuration.get(CocoonCommandLineProcessor.CompilerOptions.WrappingFunction) + configuration.get(CocoonCommandLineProcessor.CompilerOptions.WrappingFunction), ) val wrappingFunctionCallableId = wrappingFunctionString.toCallableId() val messageCollector = configuration.get( CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, - MessageCollector.NONE + MessageCollector.NONE, ) IrGenerationExtension.registerExtension( @@ -31,7 +30,7 @@ public class CocoonCompilerPluginRegistrar : CompilerPluginRegistrar() { annotationName = annotationClassId, wrappingFunctionName = wrappingFunctionCallableId, messageCollector = messageCollector, - ) + ), ) } } diff --git a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt index 42c7ec0a..2a515bfc 100644 --- a/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt +++ b/build-logic/cocoon/cocoon-compiler-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/compiler/CocoonFunctionTransformer.kt @@ -33,7 +33,6 @@ internal class CocoonFunctionTransformer( private val annotation: ClassId, private val wrappingFunction: CallableId, ) : IrElementTransformerVoidWithContext() { - @OptIn(UnsafeDuringIrConstructionAPI::class) override fun visitFunctionNew(declaration: IrFunction): IrStatement { if (!declaration.hasAnnotation(annotation) || declaration.body == null) { @@ -72,7 +71,7 @@ internal class CocoonFunctionTransformer( } arguments[wrappingFunctionParameters.size - 1] = lambdaExpression } - } + }, ) } diff --git a/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts b/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts index 7f413b41..5342dd05 100644 --- a/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts +++ b/build-logic/cocoon/cocoon-gradle-plugin/build.gradle.kts @@ -1,10 +1,11 @@ -import io.gitlab.arturbosch.detekt.Detekt +import dev.detekt.gradle.Detekt import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - `kotlin-dsl` + `java-gradle-plugin` + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.detekt) } @@ -36,7 +37,6 @@ detekt { source.from(files("src/")) config.from(files("$rootDir/../detekt.yml")) buildUponDefaultConfig = true - allRules = true parallel = true } @@ -44,7 +44,6 @@ tasks.withType().configureEach { jvmTarget = JvmTarget.JVM_11.target reports { xml.required.set(false) - txt.required.set(false) sarif.required.set(false) md.required.set(false) } @@ -52,7 +51,7 @@ tasks.withType().configureEach { dependencies { // enable Ktlint formatting - add("detektPlugins", libs.plugin.detektFormatting) + detektPlugins(libs.plugin.detektKtlintWrapper) compileOnly(libs.plugin.kotlin) } diff --git a/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt index 3883e5d3..592007a2 100644 --- a/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt +++ b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonExtension.kt @@ -2,9 +2,8 @@ package io.github.reactivecircus.cocoon.gradle import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property -import org.gradle.kotlin.dsl.property public abstract class CocoonExtension internal constructor(objects: ObjectFactory) { - public val annotation: Property = objects.property() - public val wrappingFunction: Property = objects.property() + public val annotation: Property = objects.property(String::class.java) + public val wrappingFunction: Property = objects.property(String::class.java) } diff --git a/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt index fe693e5c..bb00355f 100644 --- a/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt +++ b/build-logic/cocoon/cocoon-gradle-plugin/src/main/kotlin/io/github/reactivecircus/cocoon/gradle/CocoonPlugin.kt @@ -2,21 +2,19 @@ package io.github.reactivecircus.cocoon.gradle import org.gradle.api.Project import org.gradle.api.provider.Provider -import org.gradle.kotlin.dsl.getByType import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact import org.jetbrains.kotlin.gradle.plugin.SubpluginOption public class CocoonPlugin : KotlinCompilerPluginSupportPlugin { - override fun apply(target: Project) { target.extensions.create("cocoon", CocoonExtension::class.java) } override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { val project = kotlinCompilation.target.project - val extension = project.extensions.getByType() + val extension = project.extensions.getByType(CocoonExtension::class.java) return project.provider { listOf( SubpluginOption(