├── .gitattributes ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── poko-tests ├── src │ ├── commonMain │ │ └── kotlin │ │ │ ├── data │ │ │ ├── IdThing.kt │ │ │ ├── Nested.kt │ │ │ ├── SuperclassDeclarations.kt │ │ │ ├── Simple.kt │ │ │ ├── ExplicitDeclarations.kt │ │ │ ├── Complex.kt │ │ │ └── AnyArrayHolder.kt │ │ │ ├── poko │ │ │ ├── Expected.kt │ │ │ ├── Nested.kt │ │ │ ├── SuperclassDeclarations.kt │ │ │ ├── GenericArrayHolder.kt │ │ │ ├── SkippedProperty.kt │ │ │ ├── ComplexGenericArrayHolder.kt │ │ │ ├── Simple.kt │ │ │ ├── OnlyToString.kt │ │ │ ├── AnyArrayHolder.kt │ │ │ ├── OnlyEqualsAndHashCode.kt │ │ │ ├── ExplicitDeclarations.kt │ │ │ ├── SimpleWithExtraParam.kt │ │ │ ├── SuperclassWithFinalOverrides.kt │ │ │ ├── Complex.kt │ │ │ └── ArrayHolder.kt │ │ │ ├── Super.kt │ │ │ └── performance │ │ │ ├── IntAndLong.kt │ │ │ ├── UIntAndLong.kt │ │ │ └── Main.kt │ ├── jsMain │ │ └── kotlin │ │ │ └── poko │ │ │ └── Expected.js.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── poko │ │ │ └── Expected.jvm.kt │ ├── nativeMain │ │ └── kotlin │ │ │ └── poko │ │ │ └── Expected.native.kt │ ├── wasmJsMain │ │ └── kotlin │ │ │ └── poko │ │ │ └── Expected.wasmJs.kt │ ├── wasmWasiMain │ │ └── kotlin │ │ │ └── poko │ │ │ └── Expected.wasmWasi.kt │ └── commonTest │ │ └── kotlin │ │ ├── SimpleWithExtraParamTest.kt │ │ ├── SuperclassWithFinalOverridesTest.kt │ │ ├── OnlyToStringTest.kt │ │ ├── SkippedPropertyTest.kt │ │ ├── ExpectedTest.kt │ │ ├── SimpleTest.kt │ │ ├── ComplexGenericArrayHolderTest.kt │ │ ├── NestedTest.kt │ │ ├── OnlyEqualsAndHashCodeTest.kt │ │ ├── ExplicitDeclarationsTest.kt │ │ ├── SuperclassDeclarationsTest.kt │ │ ├── GenericArrayHolderTest.kt │ │ └── ComplexTest.kt ├── performance │ ├── src │ │ └── test │ │ │ └── kotlin │ │ │ ├── sources.kt │ │ │ ├── asm.kt │ │ │ ├── JsPerformanceTest.kt │ │ │ └── JvmPerformanceTest.kt │ └── build.gradle.kts └── build.gradle.kts ├── sample ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradle.properties ├── buildSrc │ ├── build.gradle.kts │ ├── src │ │ └── main │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── drewhamilton │ │ │ └── poko │ │ │ └── sample │ │ │ └── build │ │ │ └── java.kt │ └── settings.gradle.kts ├── jvm │ ├── src │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── dev │ │ │ │ └── drewhamilton │ │ │ │ └── poko │ │ │ │ └── sample │ │ │ │ └── jvm │ │ │ │ ├── Sample.kt │ │ │ │ └── MyData.kt │ │ ├── testFixtures │ │ │ └── kotlin │ │ │ │ └── dev │ │ │ │ └── drewhamilton │ │ │ │ └── poko │ │ │ │ └── sample │ │ │ │ └── jvm │ │ │ │ └── SampleFixture.kt │ │ └── test │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── drewhamilton │ │ │ └── poko │ │ │ └── sample │ │ │ └── jvm │ │ │ └── SampleTest.kt │ └── build.gradle.kts ├── compose │ ├── src │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── dev │ │ │ │ └── drewhamilton │ │ │ │ └── poko │ │ │ │ └── sample │ │ │ │ └── compose │ │ │ │ ├── Sample.kt │ │ │ │ └── Poko.kt │ │ └── test │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── drewhamilton │ │ │ └── poko │ │ │ └── sample │ │ │ └── compose │ │ │ └── SampleTest.kt │ └── build.gradle.kts ├── mpp │ ├── src │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── dev │ │ │ │ └── drewhamilton │ │ │ │ └── poko │ │ │ │ └── sample │ │ │ │ └── mpp │ │ │ │ ├── Sample.kt │ │ │ │ └── ArraysSample.kt │ │ └── commonTest │ │ │ └── kotlin │ │ │ └── dev │ │ │ └── drewhamilton │ │ │ └── poko │ │ │ └── sample │ │ │ └── mpp │ │ │ ├── ArraysSampleTest.kt │ │ │ └── SampleTest.kt │ └── build.gradle.kts ├── properties.gradle ├── build.gradle.kts ├── gradlew.bat ├── settings.gradle.kts └── gradlew ├── poko-compiler-plugin ├── gradle.properties ├── src │ ├── test │ │ └── resources │ │ │ ├── illegal │ │ │ ├── Interface.kt │ │ │ ├── Value.kt │ │ │ ├── GenericArrayHolder.kt │ │ │ ├── OuterClass.kt │ │ │ ├── NoConstructorProperties.kt │ │ │ ├── NoPrimaryConstructor.kt │ │ │ ├── NotArrayHolder.kt │ │ │ └── Data.kt │ │ │ └── api │ │ │ ├── DataInterface.kt │ │ │ ├── Primitives.kt │ │ │ └── MultipleInterface.kt │ └── main │ │ └── kotlin │ │ └── dev │ │ └── drewhamilton │ │ └── poko │ │ ├── fir │ │ ├── PokoKey.kt │ │ ├── PokoFirExtensionRegistrar.kt │ │ ├── PokoFirExtensionSessionComponent.kt │ │ ├── PokoFirDeclarationGenerationExtension.kt │ │ └── PokoFirCheckersExtension.kt │ │ ├── CompilerOptions.kt │ │ ├── PokoAnnotationNames.kt │ │ ├── PokoFunction.kt │ │ ├── ir │ │ ├── PokoOrigin.kt │ │ ├── logging.kt │ │ ├── irHelpers.kt │ │ ├── PokoIrGenerationExtension.kt │ │ ├── PokoFunctionBodyFiller.kt │ │ ├── functionGeneration.kt │ │ ├── toStringGeneration.kt │ │ └── equalsGeneration.kt │ │ ├── PokoCommandLineProcessor.kt │ │ └── PokoCompilerPluginRegistrar.kt ├── build.gradle.kts └── api │ └── poko-compiler-plugin.api ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml └── kotlinCodeInsightSettings.xml ├── poko-gradle-plugin ├── src │ ├── test │ │ ├── fixtures │ │ │ └── simple │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ └── kotlin │ │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── User.kt │ │ │ │ └── test │ │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── UserTest.kt │ │ │ │ └── settings.gradle.kts │ │ └── kotlin │ │ │ └── dev │ │ │ └── drewhamilton │ │ │ └── poko │ │ │ └── gradle │ │ │ └── PokoGradlePluginFixtureTest.kt │ └── main │ │ └── kotlin │ │ └── dev │ │ └── drewhamilton │ │ └── poko │ │ └── gradle │ │ ├── PokoPluginExtension.kt │ │ └── PokoGradlePlugin.kt ├── api │ └── poko-gradle-plugin.api └── build.gradle.kts ├── .gitignore ├── poko-annotations ├── src │ └── commonMain │ │ └── kotlin │ │ └── dev │ │ └── drewhamilton │ │ └── poko │ │ ├── ArrayContentBased.kt │ │ ├── optInAnnotations.kt │ │ └── Poko.kt ├── build.gradle.kts └── api │ └── poko-annotations.api ├── RELEASING.md ├── gradle.properties ├── renovate.json5 ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── settings.gradle.kts ├── gradlew.bat ├── README.md └── gradlew /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat text eol=crlf 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewhamilton/Poko/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/IdThing.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | data class IdThing( 4 | val id: Long, 5 | ) 6 | -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drewhamilton/Poko/HEAD/sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /poko-compiler-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | # We want the stdlib as a compileOnly dependency. 2 | kotlin.stdlib.default.dependency=false 3 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/Expected.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | expect class Expected( 4 | value: Int 5 | ) { 6 | val value: Int 7 | } 8 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/Nested.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | class OuterClass { 4 | data class Nested( 5 | val value: String 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/Interface.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko interface Interface 7 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/SuperclassDeclarations.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import Super 4 | 5 | data class SuperclassDeclarations( 6 | val number: Number 7 | ) : Super() 8 | -------------------------------------------------------------------------------- /sample/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | 3 | // https://docs.gradle.org/8.5/release-notes.html#kotlin-dsl-improvements 4 | org.gradle.kotlin.dsl.skipMetadataVersionCheck=false 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /poko-tests/src/jsMain/kotlin/poko/Expected.js.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | actual class Expected actual constructor( 7 | actual val value: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-tests/src/jvmMain/kotlin/poko/Expected.jvm.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | actual class Expected actual constructor( 7 | actual val value: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/Value.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko @JvmInline value class Value( 7 | val id: String 8 | ) 9 | -------------------------------------------------------------------------------- /poko-tests/src/nativeMain/kotlin/poko/Expected.native.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | actual class Expected actual constructor( 7 | actual val value: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-tests/src/wasmJsMain/kotlin/poko/Expected.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | actual class Expected actual constructor( 7 | actual val value: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-tests/src/wasmWasiMain/kotlin/poko/Expected.wasmWasi.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | actual class Expected actual constructor( 7 | actual val value: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /sample/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | implementation(libs.kotlin.gradleApi) 11 | } 12 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/test/fixtures/simple/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | alias(libs.plugins.poko) 4 | } 5 | 6 | dependencies { 7 | testImplementation(libs.junit) 8 | } 9 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/Nested.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | class OuterClass { 6 | @Poko class Nested( 7 | val value: String 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/SuperclassDeclarations.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import Super 4 | import dev.drewhamilton.poko.Poko 5 | 6 | @Poko class SuperclassDeclarations( 7 | val number: Number 8 | ) : Super() 9 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/test/fixtures/simple/src/main/kotlin/com/example/User.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | class User( 7 | val name: String, 8 | val age: Int, 9 | ) 10 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/Super.kt: -------------------------------------------------------------------------------- 1 | abstract class Super { 2 | override fun equals(other: Any?): Boolean = other == true 3 | override fun hashCode(): Int = 50934 4 | override fun toString(): String = "superclass" 5 | } 6 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/GenericArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class GenericArrayHolder( 7 | @Poko.ReadArrayContent val generic: G, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/GenericArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko class GenericArrayHolder>( 6 | @Poko.ReadArrayContent val generic: G, 7 | ) 8 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/performance/IntAndLong.kt: -------------------------------------------------------------------------------- 1 | package performance 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko class IntAndLong( 7 | val int: Int, 8 | val long: Long, 9 | ) 10 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/performance/UIntAndLong.kt: -------------------------------------------------------------------------------- 1 | package performance 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko class UIntAndLong( 7 | val uint: UInt, 8 | val long: Long, 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea 3 | !.idea/codeStyles/* 4 | !.idea/kotlinCodeInsightSettings.xml 5 | 6 | # Android Studio 7 | local.properties 8 | 9 | # Gradle 10 | .gradle 11 | build/ 12 | !**/src/main/**/build/ 13 | 14 | # Kotlin 15 | .kotlin 16 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/SkippedProperty.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class SkippedProperty( 7 | val id: String, 8 | @Poko.Skip val callback: () -> Unit, 9 | ) 10 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/ComplexGenericArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class ComplexGenericArrayHolder( 7 | @Poko.ReadArrayContent val generic: G, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/OuterClass.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | class OuterClass { 7 | 8 | @Poko inner class Inner( 9 | val value: String 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/Simple.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class Simple( 7 | val int: Int, 8 | val requiredString: String, 9 | val optionalString: String? 10 | ) 11 | -------------------------------------------------------------------------------- /sample/jvm/src/main/kotlin/dev/drewhamilton/poko/sample/jvm/Sample.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.jvm 2 | 3 | @Suppress("unused") 4 | @MyData class Sample( 5 | val int: Int, 6 | val requiredString: String, 7 | val optionalString: String?, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-tests/performance/src/test/kotlin/sources.kt: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | 3 | fun jvmOutput(relativePath: String) = File("../build/classes/kotlin/jvm/main", relativePath) 4 | fun jsOutput() = File("../build/compileSync/js/main/productionExecutable/kotlin/Poko-poko-tests.js") 5 | -------------------------------------------------------------------------------- /sample/compose/src/main/kotlin/dev/drewhamilton/poko/sample/compose/Sample.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.compose 2 | 3 | @Suppress("unused") 4 | @Poko class Sample( 5 | val int: Int, 6 | val requiredString: String, 7 | val optionalString: String?, 8 | ) 9 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/fir/PokoKey.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.fir 2 | 3 | import org.jetbrains.kotlin.GeneratedDeclarationKey 4 | 5 | internal object PokoKey : GeneratedDeclarationKey() { 6 | override fun toString() = "FirPoko" 7 | } 8 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/performance/Main.kt: -------------------------------------------------------------------------------- 1 | package performance 2 | 3 | /** 4 | * An entrypoint for Kotlin/Native and Kotlin/JS. 5 | * Should use all types on which you want to assert. 6 | */ 7 | fun main() { 8 | println(IntAndLong(1, 2L) == IntAndLong(3, 4L)) 9 | } 10 | -------------------------------------------------------------------------------- /sample/jvm/src/main/kotlin/dev/drewhamilton/poko/sample/jvm/MyData.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.jvm 2 | 3 | /** 4 | * Local replacement for default Poko annotation. 5 | */ 6 | @Retention(AnnotationRetention.SOURCE) 7 | @Target(AnnotationTarget.CLASS) 8 | annotation class MyData 9 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/OnlyToString.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.IndependentFunctionsSupport 4 | import dev.drewhamilton.poko.Poko 5 | 6 | @OptIn(IndependentFunctionsSupport::class) 7 | @Poko.ToString class OnlyToString( 8 | val name: String, 9 | ) 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/NoConstructorProperties.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused", "UNUSED_PARAMETER") 6 | @Poko class NoConstructorProperties( 7 | nonProperty: String, 8 | ) { 9 | val nonParameter: String = "" 10 | } 11 | -------------------------------------------------------------------------------- /.idea/kotlinCodeInsightSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /sample/jvm/src/testFixtures/kotlin/dev/drewhamilton/poko/sample/jvm/SampleFixture.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.jvm 2 | 3 | @Suppress("unused") // Used to validate compilation works as expected 4 | val SampleFixture = Sample( 5 | int = 1, 6 | requiredString = "String", 7 | optionalString = null 8 | ) 9 | -------------------------------------------------------------------------------- /sample/mpp/src/commonMain/kotlin/dev/drewhamilton/poko/sample/mpp/Sample.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.mpp 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko class Sample( 7 | val int: Int, 8 | val requiredString: String, 9 | val optionalString: String?, 10 | ) 11 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/NoPrimaryConstructor.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko class NoPrimaryConstructor { 7 | @Suppress("ConvertSecondaryConstructorToPrimary", "UNUSED_PARAMETER") 8 | constructor(string: String) 9 | } 10 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/AnyArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class AnyArrayHolder( 7 | @Poko.ReadArrayContent val any: Any, 8 | @Poko.ReadArrayContent val nullableAny: Any?, 9 | val trailingProperty: String, 10 | ) 11 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/OnlyEqualsAndHashCode.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.IndependentFunctionsSupport 4 | import dev.drewhamilton.poko.Poko 5 | 6 | @OptIn(IndependentFunctionsSupport::class) 7 | @Poko.EqualsAndHashCode class OnlyEqualsAndHashCode( 8 | val id: Long, 9 | ) 10 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/NotArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class NotArrayHolder( 7 | @Poko.ReadArrayContent val string: String, 8 | @Poko.ReadArrayContent val int: Int, 9 | @Poko.ReadArrayContent val float: Float, 10 | ) 11 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/Simple.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | /** 4 | * A data class version of [poko.Simple], useful for comparing generated [toString], [equals], and [hashCode]. 5 | */ 6 | @Suppress("unused") 7 | data class Simple( 8 | val int: Int, 9 | val requiredString: String, 10 | val optionalString: String? 11 | ) 12 | -------------------------------------------------------------------------------- /sample/jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | id("dev.drewhamilton.poko") 4 | `java-test-fixtures` 5 | } 6 | 7 | poko { 8 | pokoAnnotation = "dev/drewhamilton/poko/sample/jvm/MyData" 9 | } 10 | 11 | dependencies { 12 | testImplementation(libs.junit) 13 | testImplementation(libs.assertk) 14 | } 15 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/api/DataInterface.kt: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | interface DataInterface { 6 | val id: String 7 | 8 | override fun equals(other: Any?): Boolean 9 | override fun hashCode(): Int 10 | } 11 | 12 | @Poko class MyData( 13 | override val id: String, 14 | ) : DataInterface 15 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/ExplicitDeclarations.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | data class ExplicitDeclarations( 4 | private val string: String 5 | ) { 6 | override fun toString() = string 7 | override fun equals(other: Any?) = other is ExplicitDeclarations && other.string.length == string.length 8 | override fun hashCode() = string.length 9 | } 10 | -------------------------------------------------------------------------------- /sample/compose/src/main/kotlin/dev/drewhamilton/poko/sample/compose/Poko.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.compose 2 | 3 | /** 4 | * Annotation used for Poko compiler plugin, which generates [equals], [hashCode], and [toString] 5 | * for the annotated class. 6 | */ 7 | @Retention(AnnotationRetention.SOURCE) 8 | @Target(AnnotationTarget.CLASS) 9 | annotation class Poko 10 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/api/Primitives.kt: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class Primitives( 7 | val string: String, 8 | val float: Float, 9 | val double: Double, 10 | val long: Long, 11 | val int: Int, 12 | val short: Short, 13 | val byte: Byte, 14 | val boolean: Boolean 15 | ) 16 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/illegal/Data.kt: -------------------------------------------------------------------------------- 1 | package illegal 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko data class Data( 7 | val string: String, 8 | val float: Float, 9 | val double: Double, 10 | val long: Long, 11 | val int: Int, 12 | val short: Short, 13 | val byte: Byte, 14 | val boolean: Boolean 15 | ) 16 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/test/fixtures/simple/src/test/kotlin/com/example/UserTest.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | class UserTest { 7 | @Test 8 | fun valuey() { 9 | val alice1 = User("alice", 25) 10 | val alice2 = User("alice", 25) 11 | assertEquals(alice1, alice2) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /poko-tests/performance/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") 3 | } 4 | 5 | dependencies { 6 | testImplementation(libs.junit) 7 | testImplementation(libs.assertk) 8 | testImplementation(libs.asm.util) 9 | } 10 | 11 | tasks.named("test") { 12 | dependsOn(":poko-tests:compileProductionExecutableKotlinJs") 13 | dependsOn(":poko-tests:compileKotlinJvm") 14 | } 15 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/CompilerOptions.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | import org.jetbrains.kotlin.config.CompilerConfigurationKey 4 | 5 | internal object CompilerOptions { 6 | val ENABLED = CompilerConfigurationKey(BuildConfig.POKO_ENABLED_OPTION_NAME) 7 | val POKO_ANNOTATION = CompilerConfigurationKey(BuildConfig.POKO_ANNOTATION_OPTION_NAME) 8 | } 9 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/ExplicitDeclarations.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko class ExplicitDeclarations( 6 | private val string: String 7 | ) { 8 | override fun toString() = string 9 | override fun equals(other: Any?) = other is ExplicitDeclarations && other.string.length == string.length 10 | override fun hashCode() = string.length 11 | } 12 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/SimpleWithExtraParam.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class SimpleWithExtraParam( 7 | val int: Int, 8 | val requiredString: String, 9 | val optionalString: String?, 10 | callback: (Unit) -> Boolean, 11 | ) { 12 | @Suppress("CanBePrimaryConstructorProperty") 13 | val callback: (Unit) -> Boolean = callback 14 | } 15 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/PokoAnnotationNames.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | import org.jetbrains.kotlin.name.Name 4 | 5 | internal object PokoAnnotationNames { 6 | val EqualsAndHashCode = Name.identifier("EqualsAndHashCode") 7 | val ToString = Name.identifier("ToString") 8 | 9 | val ReadArrayContent = Name.identifier("ReadArrayContent") 10 | val Skip = Name.identifier("Skip") 11 | } 12 | -------------------------------------------------------------------------------- /sample/mpp/src/commonMain/kotlin/dev/drewhamilton/poko/sample/mpp/ArraysSample.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.mpp 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("unused") 6 | @Poko class ArraysSample( 7 | @Poko.ReadArrayContent val primitive: ByteArray, 8 | @Poko.ReadArrayContent val standard: Array, 9 | @Poko.ReadArrayContent val nested: Array, 10 | @Poko.ReadArrayContent val runtime: Any, 11 | ) 12 | -------------------------------------------------------------------------------- /poko-annotations/src/commonMain/kotlin/dev/drewhamilton/poko/ArrayContentBased.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | /** 4 | * Legacy name for [Poko.ReadArrayContent]. 5 | */ 6 | @Deprecated( 7 | message = "Moved to @Poko.ReadArrayContent for compatibility with custom Poko annotation", 8 | replaceWith = ReplaceWith("Poko.ReadArrayContent"), 9 | level = DeprecationLevel.ERROR, 10 | ) 11 | public typealias ArrayContentBased = Poko.ReadArrayContent 12 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/test/resources/api/MultipleInterface.kt: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Poko 6 | class MultipleInterface( 7 | val int: Int, 8 | val requiredString: String, 9 | val optionalString: String?, 10 | ): FunctionalInterface, MarkerInterface { 11 | override fun getAnInt(): Int = int 12 | } 13 | 14 | interface FunctionalInterface { 15 | fun getAnInt(): Int 16 | } 17 | 18 | interface MarkerInterface 19 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/PokoFunction.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | import org.jetbrains.kotlin.name.Name 4 | import org.jetbrains.kotlin.util.OperatorNameConventions 5 | 6 | /** 7 | * Exhaustive representation of all functions Poko generates. 8 | */ 9 | internal enum class PokoFunction( 10 | val functionName: Name, 11 | ) { 12 | Equals(OperatorNameConventions.EQUALS), 13 | HashCode(OperatorNameConventions.HASH_CODE), 14 | ToString(OperatorNameConventions.TO_STRING), 15 | } 16 | -------------------------------------------------------------------------------- /sample/buildSrc/src/main/kotlin/dev/drewhamilton/poko/sample/build/java.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.build 2 | 3 | import org.gradle.api.JavaVersion 4 | import org.gradle.jvm.toolchain.JavaLanguageVersion 5 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 6 | 7 | val resolvedJavaVersion: JavaVersion = JavaVersion.VERSION_11 8 | 9 | val kotlinJvmTarget: JvmTarget = when (resolvedJavaVersion) { 10 | JavaVersion.VERSION_1_8 -> JvmTarget.JVM_1_8 11 | else -> JvmTarget.valueOf("JVM_${resolvedJavaVersion.majorVersion}") 12 | } 13 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/PokoOrigin.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import kotlin.properties.ReadOnlyProperty 4 | import kotlin.reflect.KProperty 5 | import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin 6 | 7 | internal object PokoOrigin : IrDeclarationOrigin, ReadOnlyProperty { 8 | override val name: String = "GENERATED_POKO_CLASS_MEMBER" 9 | 10 | override fun toString(): String = name 11 | 12 | override fun getValue(thisRef: Any?, property: KProperty<*>): PokoOrigin = this 13 | } 14 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/fir/PokoFirExtensionRegistrar.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.fir 2 | 3 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar 4 | import org.jetbrains.kotlin.name.ClassId 5 | 6 | internal class PokoFirExtensionRegistrar( 7 | private val pokoAnnotation: ClassId, 8 | ) : FirExtensionRegistrar() { 9 | override fun ExtensionRegistrarContext.configurePlugin() { 10 | +PokoFirExtensionSessionComponent.getFactory(pokoAnnotation) 11 | +::PokoFirCheckersExtension 12 | +::PokoFirDeclarationGenerationExtension 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /poko-tests/performance/src/test/kotlin/asm.kt: -------------------------------------------------------------------------------- 1 | import java.io.PrintWriter 2 | import java.io.StringWriter 3 | import org.objectweb.asm.ClassReader 4 | import org.objectweb.asm.util.Textifier 5 | import org.objectweb.asm.util.TraceClassVisitor 6 | 7 | fun bytecodeToText(bytecode: ByteArray): String { 8 | return ClassReader(bytecode).toText() 9 | } 10 | 11 | fun ClassReader.toText(): String { 12 | val textifier = Textifier() 13 | accept(TraceClassVisitor(null, textifier, null), 0) 14 | 15 | val writer = StringWriter() 16 | textifier.print(PrintWriter(writer)) 17 | return writer.toString().trim() 18 | } 19 | -------------------------------------------------------------------------------- /sample/mpp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) 3 | id("dev.drewhamilton.poko") 4 | } 5 | 6 | kotlin { 7 | js().nodejs() 8 | 9 | jvm() 10 | 11 | // All native "desktop" platforms to ensure at least one set of native tests will run. 12 | linuxArm64() 13 | linuxX64() 14 | macosArm64() 15 | macosX64() 16 | mingwX64() 17 | 18 | sourceSets { 19 | commonTest { 20 | dependencies { 21 | implementation(libs.kotlin.test) 22 | implementation(libs.assertk) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/SuperclassWithFinalOverrides.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | open class SuperclassWithFinalOverrides( 6 | private val id: String, 7 | ) { 8 | final override fun equals(other: Any?): Boolean = when { 9 | this === other -> true 10 | other !is SuperclassWithFinalOverrides -> false 11 | else -> this.id == other.id 12 | } 13 | 14 | final override fun hashCode(): Int = 31 + id.hashCode() 15 | 16 | final override fun toString(): String = id 17 | 18 | @Poko class Subclass( 19 | val name: String, 20 | ) : SuperclassWithFinalOverrides(id = "Subclass") 21 | } 22 | 23 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/test/fixtures/simple/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | setUrl(rootDir.resolve("../../../../../build/localMaven")) 5 | } 6 | mavenCentral() 7 | } 8 | } 9 | 10 | dependencyResolutionManagement { 11 | versionCatalogs.register("libs") { 12 | from(files("../../../../../gradle/libs.versions.toml")) 13 | plugin("poko", "dev.drewhamilton.poko").version(providers.gradleProperty("pokoVersion").get()) 14 | } 15 | 16 | repositories { 17 | maven { 18 | setUrl(rootDir.resolve("../../../../../build/localMaven")) 19 | } 20 | mavenCentral() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample/buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | create("libs") { 4 | from(files("../../gradle/libs.versions.toml")) 5 | 6 | //region Duplicated in ../settings.gradle 7 | fun String.nullIfBlank(): String? = if (isNullOrBlank()) null else this 8 | 9 | // Compile sample project with different Kotlin version than Poko, if provided: 10 | val kotlinVersionOverride = System.getenv()["poko_sample_kotlin_version"]?.nullIfBlank() 11 | kotlinVersionOverride?.let { kotlinVersion -> 12 | version("kotlin", kotlinVersion) 13 | } 14 | //endregion 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/SimpleWithExtraParamTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.assertThat 2 | import assertk.assertions.hashCodeFun 3 | import assertk.assertions.isEqualTo 4 | import assertk.assertions.toStringFun 5 | import kotlin.test.Test 6 | import poko.SimpleWithExtraParam 7 | 8 | class SimpleWithExtraParamTest { 9 | @Test fun nonproperty_parameter_is_ignored_for_equals() { 10 | val a = SimpleWithExtraParam(1, "String", null, { true }) 11 | val b = SimpleWithExtraParam(1, "String", null, { false }) 12 | assertThat(a).isEqualTo(b) 13 | assertThat(b).isEqualTo(a) 14 | assertThat(a).hashCodeFun().isEqualTo(b.hashCode()) 15 | assertThat(a).toStringFun().isEqualTo(b.toString()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/Complex.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class Complex( 7 | val referenceType: String, 8 | val nullableReferenceType: String?, 9 | val boolean: Boolean, 10 | val nullableBoolean: Boolean?, 11 | val int: Int, 12 | val nullableInt: Int?, 13 | val long: Long, 14 | val float: Float, 15 | val double: Double, 16 | val arrayReferenceType: Array, 17 | val nullableArrayReferenceType: Array?, 18 | val arrayPrimitiveType: IntArray, 19 | val nullableArrayPrimitiveType: IntArray?, 20 | val genericCollectionType: List, 21 | val nullableGenericCollectionType: List?, 22 | val genericType: T, 23 | val nullableGenericType: T? 24 | ) 25 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/SuperclassWithFinalOverridesTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.toStringFun 6 | import kotlin.test.Test 7 | import poko.SuperclassWithFinalOverrides 8 | 9 | class SuperclassWithFinalOverridesTest { 10 | 11 | @Test fun successful_instantiation_with_final_function_overrides_in_superclass() { 12 | val instance = SuperclassWithFinalOverrides.Subclass(name = "this-is-fine") 13 | assertThat(instance).all { 14 | toStringFun().isEqualTo("Subclass") 15 | hashCodeFun().isEqualTo(31 + "Subclass".hashCode()) 16 | isEqualTo(SuperclassWithFinalOverrides.Subclass(name = "different-name")) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/OnlyToStringTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isNotEqualTo 6 | import kotlin.test.Test 7 | import poko.OnlyToString 8 | 9 | class OnlyToStringTest { 10 | @Test fun two_equivalent_compiled_OnlyToString_instances_are_not_equals() { 11 | val a = OnlyToString(name = "Only toString") 12 | val b = OnlyToString(name = "Only toString") 13 | assertThat(a).isNotEqualTo(b) 14 | assertThat(b).isNotEqualTo(a) 15 | assertThat(a).hashCodeFun().isNotEqualTo(b.hashCode()) 16 | } 17 | 18 | @Test fun onlyToString_instance_has_expected_toString() { 19 | assertThat(OnlyToString("Title").toString()).isEqualTo("OnlyToString(name=Title)") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Make sure you're on the latest commit on the main branch. 4 | 2. Update CHANGELOG.md for the impending release. 5 | 3. Change `PUBLISH_VERSION` in gradle.properties to a non-SNAPSHOT version. 6 | 4. Update README.md for the impending release. 7 | 5. Commit (don't push) the changes with message "Release x.y.z", where x.y.z is the new version. 8 | 6. Tag the commit `x.y.z`, where x.y.z is the new version. 9 | 7. Change `PUBLISH_VERSION` in gradle.properties to the next SNAPSHOT version. 10 | 8. Commit the snapshot change. 11 | 9. Push the tag and 2 commits to origin/main. 12 | 10. Wait for the "Release" Action to complete. 13 | 11. Create the release on GitHub with release notes copied from the changelog. 14 | 15 | If steps 10 fails: drop the Sonatype repo, fix the problem, delete the incorrect tag on both local 16 | and remote, and start over. 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | # Do not include DOM compatibility dependency for JS targets. 4 | # See https://youtrack.jetbrains.com/issue/KT-35973 for more info. 5 | kotlin.js.stdlib.dom.api.included=false 6 | 7 | PUBLISH_GROUP=dev.drewhamilton.poko 8 | PUBLISH_VERSION=0.22.0-SNAPSHOT 9 | 10 | # Uncomment to enable snapshot dependencies: 11 | #snapshots_repository=https://oss.sonatype.org/content/repositories/snapshots 12 | # Uncomment to enable dev versions of Kotlin dependencies: 13 | #kotlin_dev_repository=https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev 14 | 15 | # Workaround for https://github.com/Kotlin/dokka/issues/1405 on `./gradlew dokkaJavadoc` 16 | org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m 17 | 18 | # Dokka v2 19 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 20 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 21 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/Complex.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | /** 4 | * A data class version of [poko.Complex], useful for comparing generated [toString], [equals], and [hashCode]. 5 | */ 6 | @Suppress("Unused", "ArrayInDataClass") 7 | data class Complex( 8 | val referenceType: String, 9 | val nullableReferenceType: String?, 10 | val boolean: Boolean, 11 | val nullableBoolean: Boolean?, 12 | val int: Int, 13 | val nullableInt: Int?, 14 | val long: Long, 15 | val float: Float, 16 | val double: Double, 17 | val arrayReferenceType: Array, 18 | val nullableArrayReferenceType: Array?, 19 | val arrayPrimitiveType: IntArray, 20 | val nullableArrayPrimitiveType: IntArray?, 21 | val genericCollectionType: List, 22 | val nullableGenericCollectionType: List?, 23 | val genericType: T, 24 | val nullableGenericType: T? 25 | ) 26 | -------------------------------------------------------------------------------- /poko-tests/performance/src/test/kotlin/JsPerformanceTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import assertk.assertAll 3 | import assertk.assertThat 4 | import assertk.assertions.hasSize 5 | import assertk.assertions.isEmpty 6 | import org.junit.Test 7 | 8 | class JsPerformanceTest { 9 | @Test fun `equals does not perform redundant instanceof check`() { 10 | val javascript = jsOutput().readText() 11 | 12 | // Hack to filter out data classes, which do have the `THROW_CCE` code: 13 | val intAndLongLines = javascript.split("\n").filter { it.contains("IntAndLong") } 14 | assertAll { 15 | assertThat( 16 | actual = intAndLongLines.filter { it.contains("other instanceof IntAndLong") }, 17 | ).hasSize(1) 18 | 19 | assertThat( 20 | actual = intAndLongLines.filter { it.contains("THROW_CCE") }, 21 | ).isEmpty() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /poko-annotations/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.multiplatform") 5 | } 6 | 7 | pokoBuild { 8 | publishing("Poko Annotations") 9 | enableBackwardsCompatibility() 10 | } 11 | 12 | kotlin { 13 | jvm() 14 | 15 | js().nodejs() 16 | 17 | mingwX64() 18 | 19 | linuxArm64() 20 | linuxX64() 21 | 22 | iosArm64() 23 | iosSimulatorArm64() 24 | iosX64() 25 | 26 | macosArm64() 27 | macosX64() 28 | 29 | tvosArm64() 30 | tvosX64() 31 | tvosSimulatorArm64() 32 | 33 | @OptIn(ExperimentalWasmDsl::class) 34 | wasmJs().nodejs() 35 | @OptIn(ExperimentalWasmDsl::class) 36 | wasmWasi().nodejs() 37 | 38 | watchosArm32() 39 | watchosArm64() 40 | watchosDeviceArm64() 41 | watchosSimulatorArm64() 42 | watchosX64() 43 | 44 | androidNativeArm32() 45 | androidNativeArm64() 46 | androidNativeX86() 47 | androidNativeX64() 48 | } 49 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/SkippedPropertyTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import assertk.assertAll 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import kotlin.test.Test 6 | import poko.SkippedProperty 7 | 8 | class SkippedPropertyTest { 9 | 10 | @Test fun skipped_property_omitted_from_all_generated_functions() { 11 | val a = SkippedProperty( 12 | id = "id", 13 | callback = { println("Callback invoked") }, 14 | ) 15 | val b = SkippedProperty( 16 | id = "id", 17 | callback = { println("Callback invoked") }, 18 | ) 19 | 20 | assertAll { 21 | assertThat(a).isEqualTo(b) 22 | assertThat(b).isEqualTo(a) 23 | assertThat(a.hashCode()).isEqualTo(b.hashCode()) 24 | assertThat(a.toString()).isEqualTo(b.toString()) 25 | assertThat(a.toString()).isEqualTo("SkippedProperty(id=id)") 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /poko-annotations/src/commonMain/kotlin/dev/drewhamilton/poko/optInAnnotations.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | /** 4 | * Denotes an experimental API that allows generating Poko functions individually; i.e. a class 5 | * could have `toString` or it could have `equals` and `hashCode`, without having all three. 6 | */ 7 | @RequiresOptIn 8 | public annotation class IndependentFunctionsSupport 9 | 10 | /** 11 | * Denotes an experimental API that enables the ability to skip a Poko class primary constructor 12 | * property when generating Poko functions. 13 | */ 14 | @Deprecated( 15 | message = "Skip support no longer requires opt-in", 16 | ) 17 | @RequiresOptIn 18 | public annotation class SkipSupport 19 | 20 | /** 21 | * Denotes an experimental API that enables support for array content reading. 22 | */ 23 | @Deprecated( 24 | message = "Array content support no longer requires opt-in", 25 | level = DeprecationLevel.ERROR, 26 | ) 27 | @RequiresOptIn 28 | public annotation class ArrayContentSupport 29 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/ExpectedTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import assertk.assertThat 3 | import assertk.assertions.isEqualTo 4 | import assertk.assertions.isNotEqualTo 5 | import kotlin.test.Test 6 | import poko.Expected 7 | 8 | class ExpectedTest { 9 | @Test fun two_equivalent_compiled_Expected_instances_are_equals() { 10 | val a = Expected(1) 11 | val b = Expected(1) 12 | assertThat(a).isEqualTo(b) 13 | assertThat(b).isEqualTo(a) 14 | assertThat(a.hashCode()).isEqualTo(b.hashCode()) 15 | } 16 | 17 | @Test fun two_inequivalent_compiled_Expected_instances_are_not_equals() { 18 | val a = Expected(2) 19 | val b = Expected(3) 20 | assertThat(a).isNotEqualTo(b) 21 | assertThat(b).isNotEqualTo(a) 22 | assertThat(a.hashCode()).isNotEqualTo(b.hashCode()) 23 | } 24 | 25 | @Test fun compiled_Expected_class_instance_has_expected_toString() { 26 | val actual = Expected(4) 27 | assertThat(actual.toString()).isEqualTo("Expected(value=4)") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /poko-gradle-plugin/api/poko-gradle-plugin.api: -------------------------------------------------------------------------------- 1 | public final class dev/drewhamilton/poko/gradle/PokoGradlePlugin : org/jetbrains/kotlin/gradle/plugin/KotlinCompilerPluginSupportPlugin { 2 | public fun ()V 3 | public synthetic fun apply (Ljava/lang/Object;)V 4 | public fun apply (Lorg/gradle/api/Project;)V 5 | public fun applyToCompilation (Lorg/jetbrains/kotlin/gradle/plugin/KotlinCompilation;)Lorg/gradle/api/provider/Provider; 6 | public fun getCompilerPluginId ()Ljava/lang/String; 7 | public fun getPluginArtifact ()Lorg/jetbrains/kotlin/gradle/plugin/SubpluginArtifact; 8 | public fun getPluginArtifactForNative ()Lorg/jetbrains/kotlin/gradle/plugin/SubpluginArtifact; 9 | public fun isApplicable (Lorg/jetbrains/kotlin/gradle/plugin/KotlinCompilation;)Z 10 | } 11 | 12 | public abstract class dev/drewhamilton/poko/gradle/PokoPluginExtension { 13 | public fun (Lorg/gradle/api/model/ObjectFactory;)V 14 | public final fun getEnabled ()Lorg/gradle/api/provider/Property; 15 | public final fun getPokoAnnotation ()Lorg/gradle/api/provider/Property; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /poko-annotations/api/poko-annotations.api: -------------------------------------------------------------------------------- 1 | public abstract interface annotation class dev/drewhamilton/poko/ArrayContentSupport : java/lang/annotation/Annotation { 2 | } 3 | 4 | public abstract interface annotation class dev/drewhamilton/poko/IndependentFunctionsSupport : java/lang/annotation/Annotation { 5 | } 6 | 7 | public abstract interface annotation class dev/drewhamilton/poko/Poko : java/lang/annotation/Annotation { 8 | } 9 | 10 | public abstract interface annotation class dev/drewhamilton/poko/Poko$EqualsAndHashCode : java/lang/annotation/Annotation { 11 | } 12 | 13 | public abstract interface annotation class dev/drewhamilton/poko/Poko$ReadArrayContent : java/lang/annotation/Annotation { 14 | } 15 | 16 | public abstract interface annotation class dev/drewhamilton/poko/Poko$Skip : java/lang/annotation/Annotation { 17 | } 18 | 19 | public abstract interface annotation class dev/drewhamilton/poko/Poko$ToString : java/lang/annotation/Annotation { 20 | } 21 | 22 | public abstract interface annotation class dev/drewhamilton/poko/SkipSupport : java/lang/annotation/Annotation { 23 | } 24 | 25 | -------------------------------------------------------------------------------- /poko-compiler-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("org.jetbrains.kotlin.jvm") 5 | alias(libs.plugins.ksp) 6 | } 7 | 8 | pokoBuild { 9 | publishing("Poko Compiler Plugin") 10 | generateBuildConfig("dev.drewhamilton.poko") 11 | } 12 | 13 | dependencies { 14 | // The stdlib and compiler APIs will be provided by the enclosing Kotlin compiler environment. 15 | compileOnly(libs.kotlin.stdlib) 16 | compileOnly(libs.kotlin.embeddableCompiler) 17 | 18 | compileOnly(libs.autoService.annotations) 19 | ksp(libs.autoService.ksp) 20 | 21 | testImplementation(project(":poko-annotations")) 22 | testImplementation(libs.kotlin.embeddableCompiler) 23 | testImplementation(libs.junit) 24 | testImplementation(libs.assertk) 25 | testImplementation(libs.testParameterInjector) 26 | testImplementation(libs.kotlinCompileTestingFork) 27 | } 28 | 29 | tasks.withType().configureEach { 30 | compilerOptions { 31 | freeCompilerArgs.add("-Xcontext-parameters") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: 'https://docs.renovatebot.com/renovate-schema.json', 3 | extends: [ 4 | 'config:recommended', 5 | ], 6 | configMigration: true, 7 | packageRules: [ 8 | { 9 | // Compiler tools are tightly coupled to Kotlin version: 10 | groupName: 'Kotlin', 11 | matchPackageNames: [ 12 | 'androidx.compose.compiler{/,}**', 13 | 'com.google.devtools.ksp{/,}**', 14 | 'com.github.tschuchortdev:kotlin-compile-testing{/,}**', 15 | 'dev.zacsweers.kctfork{/,}**', 16 | 'org.jetbrains.kotlin{/,}**', 17 | 'org.jetbrains.kotlinx:binary-compatibility-validator{/,}**', 18 | ], 19 | }, 20 | { 21 | groupName: 'Upload/download artifact', 22 | matchPackageNames: [ 23 | 'actions/download-artifact', 24 | 'actions/upload-artifact', 25 | ], 26 | }, 27 | ], 28 | ignoreDeps: [ 29 | // These should just match the main Kotlin version: 30 | 'org.jetbrains.kotlin:kotlin-compiler-embeddable', 31 | 'org.jetbrains.kotlin:kotlin-gradle-plugin-api', 32 | 'org.jetbrains.kotlin:kotlin-stdlib', 33 | 'org.jetbrains.kotlin:kotlin-test', 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ "*" ] 7 | 8 | jobs: 9 | release: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Check out the repo 14 | uses: actions/checkout@v6 15 | 16 | - name: Install JDK 17 | uses: actions/setup-java@v5 18 | with: 19 | distribution: zulu 20 | java-version: | 21 | 24 22 | 25 23 | 24 | - name: Set up Gradle 25 | uses: gradle/actions/setup-gradle@v5 26 | 27 | - name: Assemble for release 28 | run: ./gradlew assemble 29 | - name: Publish 30 | run: ./gradlew publish --no-configuration-cache 31 | env: 32 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_personalSonatypeIssuesUsername }} 33 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_personalSonatypeIssuesPassword }} 34 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_personalGpgKey }} 35 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_personalGpgPassword }} 36 | -------------------------------------------------------------------------------- /sample/compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.drewhamilton.poko.sample.build.resolvedJavaVersion 2 | 3 | plugins { 4 | alias(libs.plugins.android.library) 5 | alias(libs.plugins.kotlin.android) 6 | alias(libs.plugins.kotlin.compose) 7 | id("dev.drewhamilton.poko") 8 | } 9 | 10 | poko { 11 | pokoAnnotation.set("dev/drewhamilton/poko/sample/compose/Poko") 12 | } 13 | 14 | android { 15 | namespace = "dev.drewhamilton.poko.sample.compose" 16 | compileSdk = 36 17 | 18 | defaultConfig { 19 | minSdk = 21 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility(resolvedJavaVersion) 24 | targetCompatibility(resolvedJavaVersion) 25 | } 26 | 27 | kotlin { 28 | compilerOptions { 29 | progressiveMode.set(true) 30 | } 31 | } 32 | 33 | buildFeatures { 34 | compose = true 35 | 36 | // Disable unused AGP features 37 | resValues = false 38 | shaders = false 39 | } 40 | 41 | androidResources.enable = false 42 | } 43 | 44 | dependencies { 45 | implementation(libs.androidx.compose.runtime) 46 | 47 | testImplementation(libs.junit) 48 | testImplementation(libs.assertk) 49 | } 50 | 51 | repositories { 52 | google() 53 | } 54 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("build-support") 3 | 4 | //region TODO: Move this to build-support 5 | val file: File = file("gradle.properties") 6 | val properties = java.util.Properties() 7 | file.inputStream().use { 8 | properties.load(it) 9 | } 10 | properties.forEach { (key, value) -> 11 | extra[key.toString()] = value 12 | } 13 | //endregion 14 | 15 | repositories { 16 | mavenCentral() 17 | 18 | // KSP: 19 | google() 20 | 21 | // buildconfig plugin: 22 | gradlePluginPortal() 23 | 24 | if (extra.has("kotlin_dev_repository")) { 25 | val kotlinDevRepository = extra["kotlin_dev_repository"]!! 26 | logger.lifecycle("Adding <$kotlinDevRepository> repository for plugins") 27 | maven { url = uri(kotlinDevRepository) } 28 | } 29 | } 30 | } 31 | 32 | plugins { 33 | id("dev.drewhamilton.poko.settings") 34 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 35 | } 36 | 37 | rootProject.name = "Poko" 38 | 39 | include( 40 | ":poko-compiler-plugin", 41 | ":poko-annotations", 42 | ":poko-gradle-plugin", 43 | ":poko-tests", 44 | ":poko-tests:performance", 45 | ) 46 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/SimpleTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isNotEqualTo 6 | import assertk.assertions.toStringFun 7 | import kotlin.test.Test 8 | import data.Simple as SimpleData 9 | import poko.Simple as SimplePoko 10 | 11 | class SimpleTest { 12 | @Test fun two_equivalent_compiled_Simple_instances_are_equals() { 13 | val a = SimplePoko(1, "String", null) 14 | val b = SimplePoko(1, "String", null) 15 | assertThat(a).isEqualTo(b) 16 | assertThat(b).isEqualTo(a) 17 | } 18 | 19 | @Test fun two_inequivalent_compiled_Simple_instances_are_not_equals() { 20 | val a = SimplePoko(1, "String", null) 21 | val b = SimplePoko(1, "String", "non-null") 22 | assertThat(a).isNotEqualTo(b) 23 | assertThat(b).isNotEqualTo(a) 24 | } 25 | 26 | @Test fun compiled_Simple_class_instance_has_expected_hashCode() { 27 | val poko = SimplePoko(1, "String", null) 28 | val data = SimpleData(1, "String", null) 29 | assertThat(poko).all { 30 | hashCodeFun().isEqualTo(data.hashCode()) 31 | toStringFun().isEqualTo(data.toString()) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/main/kotlin/dev/drewhamilton/poko/gradle/PokoPluginExtension.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.gradle 2 | 3 | import dev.drewhamilton.poko.gradle.BuildConfig.DEFAULT_POKO_ANNOTATION 4 | import dev.drewhamilton.poko.gradle.BuildConfig.DEFAULT_POKO_ENABLED 5 | import javax.inject.Inject 6 | import org.gradle.api.model.ObjectFactory 7 | import org.gradle.api.provider.Property 8 | 9 | public abstract class PokoPluginExtension @Inject constructor(objects: ObjectFactory) { 10 | 11 | public val enabled: Property = objects.property(Boolean::class.javaObjectType) 12 | .convention(DEFAULT_POKO_ENABLED) 13 | 14 | /** 15 | * Define a custom Poko marker annotation. The poko-annotations artifact won't be automatically 16 | * added as a dependency if a different annotation is defined. 17 | * 18 | * Note that this must be in the format of a string where packages are delimited by `/` and 19 | * classes by `.`, e.g. `com/example/Nested.Annotation`. 20 | * 21 | * Note that this affects the main Poko annotation and any nested annotations, such as 22 | * `@Poko.ReadArrayContent` and `@Poko.Skip`. 23 | */ 24 | public val pokoAnnotation: Property = objects.property(String::class.java) 25 | .convention(DEFAULT_POKO_ANNOTATION) 26 | } 27 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/ComplexGenericArrayHolderTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.assertThat 2 | import assertk.assertions.hashCodeFun 3 | import assertk.assertions.isEqualTo 4 | import assertk.assertions.toStringFun 5 | import kotlin.test.Test 6 | import poko.ComplexGenericArrayHolder 7 | 8 | class ComplexGenericArrayHolderTest { 9 | @Test fun two_ComplexGenericArrayHolder_instances_with_equivalent_int_arrays_are_equals() { 10 | val a = ComplexGenericArrayHolder( 11 | generic = intArrayOf(50, 100), 12 | ) 13 | val b = ComplexGenericArrayHolder( 14 | generic = intArrayOf(50, 100), 15 | ) 16 | assertThat(a).isEqualTo(b) 17 | assertThat(b).isEqualTo(a) 18 | } 19 | 20 | @Test fun hashCode_produces_expected_value() { 21 | val value = ComplexGenericArrayHolder( 22 | generic = intArrayOf(50, 100), 23 | ) 24 | // Ensure consistency across platforms: 25 | assertThat(value).hashCodeFun().isEqualTo(2611) 26 | } 27 | 28 | @Test fun toString_produces_expected_value() { 29 | val value = ComplexGenericArrayHolder( 30 | generic = intArrayOf(50, 100), 31 | ) 32 | assertThat(value).toStringFun().isEqualTo("ComplexGenericArrayHolder(generic=[50, 100])") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/NestedTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isNotEqualTo 6 | import assertk.assertions.toStringFun 7 | import kotlin.test.Test 8 | import data.OuterClass.Nested as NestedData 9 | import poko.OuterClass.Nested as NestedPoko 10 | 11 | class NestedTest { 12 | @Test fun two_equivalent_compiled_Nested_instances_are_equals() { 13 | val a = NestedPoko("string 1") 14 | val b = NestedPoko("string 1") 15 | assertThat(a).isEqualTo(b) 16 | assertThat(b).isEqualTo(a) 17 | } 18 | 19 | @Test fun two_inequivalent_compiled_Nested_instances_are_not_equals() { 20 | val a = NestedPoko("string 1") 21 | val b = NestedPoko("string 2") 22 | assertThat(a).isNotEqualTo(b) 23 | assertThat(b).isNotEqualTo(a) 24 | } 25 | 26 | @Test fun compilation_of_nested_class_within_class_matches_corresponding_data_class_hashCode() { 27 | val poko = NestedPoko("nested class value") 28 | val data = NestedData("nested class value") 29 | assertThat(poko).all { 30 | hashCodeFun().isEqualTo(data.hashCode()) 31 | toStringFun().isEqualTo(data.toString()) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/logging.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 4 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 5 | import org.jetbrains.kotlin.cli.common.messages.MessageUtil 6 | import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI 7 | import org.jetbrains.kotlin.ir.declarations.IrClass 8 | import org.jetbrains.kotlin.ir.declarations.IrProperty 9 | import org.jetbrains.kotlin.resolve.source.getPsi 10 | 11 | internal fun MessageCollector.log(message: String) { 12 | report(CompilerMessageSeverity.LOGGING, "POKO COMPILER PLUGIN (IR): $message") 13 | } 14 | 15 | internal fun MessageCollector.reportErrorOnClass(irClass: IrClass, message: String) { 16 | val psi = irClass.source.getPsi() 17 | val location = MessageUtil.psiElementToMessageLocation(psi) 18 | report(CompilerMessageSeverity.ERROR, message, location) 19 | } 20 | 21 | @OptIn(ObsoleteDescriptorBasedAPI::class) // Only needed for non-K2 compilation 22 | internal fun MessageCollector.reportErrorOnProperty(property: IrProperty, message: String) { 23 | val psi = property.descriptor.source.getPsi() 24 | val location = MessageUtil.psiElementToMessageLocation(psi) 25 | report(CompilerMessageSeverity.ERROR, message, location) 26 | } 27 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/poko/ArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package poko 2 | 3 | import dev.drewhamilton.poko.Poko 4 | 5 | @Suppress("Unused") 6 | @Poko class ArrayHolder( 7 | @Poko.ReadArrayContent val stringArray: Array, 8 | @Poko.ReadArrayContent val nullableStringArray: Array?, 9 | @Poko.ReadArrayContent val booleanArray: BooleanArray, 10 | @Poko.ReadArrayContent val nullableBooleanArray: BooleanArray?, 11 | @Poko.ReadArrayContent val byteArray: ByteArray, 12 | @Poko.ReadArrayContent val nullableByteArray: ByteArray?, 13 | @Poko.ReadArrayContent val charArray: CharArray, 14 | @Poko.ReadArrayContent val nullableCharArray: CharArray?, 15 | @Poko.ReadArrayContent val shortArray: ShortArray, 16 | @Poko.ReadArrayContent val nullableShortArray: ShortArray?, 17 | @Poko.ReadArrayContent val intArray: IntArray, 18 | @Poko.ReadArrayContent val nullableIntArray: IntArray?, 19 | @Poko.ReadArrayContent val longArray: LongArray, 20 | @Poko.ReadArrayContent val nullableLongArray: LongArray?, 21 | @Poko.ReadArrayContent val floatArray: FloatArray, 22 | @Poko.ReadArrayContent val nullableFloatArray: FloatArray?, 23 | @Poko.ReadArrayContent val doubleArray: DoubleArray, 24 | @Poko.ReadArrayContent val nullableDoubleArray: DoubleArray?, 25 | @Poko.ReadArrayContent val nestedStringArray: Array>, 26 | @Poko.ReadArrayContent val nestedIntArray: Array, 27 | ) 28 | -------------------------------------------------------------------------------- /poko-compiler-plugin/api/poko-compiler-plugin.api: -------------------------------------------------------------------------------- 1 | public final class dev/drewhamilton/poko/PokoCommandLineProcessor : org/jetbrains/kotlin/compiler/plugin/CommandLineProcessor { 2 | public fun ()V 3 | public fun appendList (Lorg/jetbrains/kotlin/config/CompilerConfiguration;Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;Ljava/lang/Object;)V 4 | public fun appendList (Lorg/jetbrains/kotlin/config/CompilerConfiguration;Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;Ljava/util/List;)V 5 | public fun applyOptionsFrom (Lorg/jetbrains/kotlin/config/CompilerConfiguration;Ljava/util/Map;Ljava/util/Collection;)V 6 | public fun getPluginId ()Ljava/lang/String; 7 | public fun getPluginOptions ()Ljava/util/Collection; 8 | public fun processOption (Lorg/jetbrains/kotlin/compiler/plugin/AbstractCliOption;Ljava/lang/String;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V 9 | public fun processOption (Lorg/jetbrains/kotlin/compiler/plugin/CliOption;Ljava/lang/String;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V 10 | } 11 | 12 | public final class dev/drewhamilton/poko/PokoCompilerPluginRegistrar : org/jetbrains/kotlin/compiler/plugin/CompilerPluginRegistrar { 13 | public fun ()V 14 | public fun getPluginId ()Ljava/lang/String; 15 | public fun getSupportsK2 ()Z 16 | public fun registerExtensions (Lorg/jetbrains/kotlin/compiler/plugin/CompilerPluginRegistrar$ExtensionStorage;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V 17 | } 18 | 19 | -------------------------------------------------------------------------------- /sample/properties.gradle: -------------------------------------------------------------------------------- 1 | // Copies main project Gradle properties to sample project. Can't use buildSrc because settings.gradle 2 | // must access these properties. 3 | 4 | List propertiesFiles = [ 5 | "../gradle.properties", 6 | "../local.properties", 7 | ] 8 | 9 | propertiesFiles.each { fileName -> 10 | File file = file(fileName) 11 | if (file.exists()) { 12 | Properties properties = new Properties() 13 | file.withInputStream { properties.load(it) } 14 | 15 | int moduleNameStartIndex = fileName.indexOf('/') + 1 16 | int moduleNameEndIndex = fileName.lastIndexOf('/') 17 | String namespace 18 | if (moduleNameStartIndex < moduleNameEndIndex) { 19 | namespace = fileName.substring(moduleNameStartIndex, moduleNameEndIndex) 20 | .replace('/', '.') 21 | } else { 22 | namespace = null 23 | } 24 | 25 | properties.each { key, value -> 26 | String namespacedKey 27 | if (namespace == null) { 28 | namespacedKey = key 29 | } else { 30 | namespacedKey = "$namespace.$key" 31 | } 32 | try { 33 | properties.set(namespacedKey, value) 34 | } catch (MissingMethodException ignored) { 35 | // We are in a pluginManagement block that can't set properties, so set an extra instead: 36 | ext.set(namespacedKey, value) 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/fir/PokoFirExtensionSessionComponent.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.fir 2 | 3 | import dev.drewhamilton.poko.PokoAnnotationNames 4 | import org.jetbrains.kotlin.fir.FirSession 5 | import org.jetbrains.kotlin.fir.extensions.FirExtensionSessionComponent 6 | import org.jetbrains.kotlin.name.ClassId 7 | 8 | internal class PokoFirExtensionSessionComponent( 9 | session: FirSession, 10 | internal val pokoAnnotation: ClassId, 11 | ) : FirExtensionSessionComponent(session) { 12 | 13 | internal val pokoEqualsAndHashCodeAnnotation: ClassId = 14 | pokoAnnotation.createNestedClassId(PokoAnnotationNames.EqualsAndHashCode) 15 | 16 | internal val pokoToStringAnnotation: ClassId = 17 | pokoAnnotation.createNestedClassId(PokoAnnotationNames.ToString) 18 | 19 | internal val pokoReadArrayContentAnnotation: ClassId = 20 | pokoAnnotation.createNestedClassId(PokoAnnotationNames.ReadArrayContent) 21 | 22 | internal val pokoSkipAnnotation: ClassId = 23 | pokoAnnotation.createNestedClassId(PokoAnnotationNames.Skip) 24 | 25 | internal companion object { 26 | internal fun getFactory(pokoAnnotation: ClassId): Factory { 27 | return Factory { session -> 28 | PokoFirExtensionSessionComponent(session, pokoAnnotation) 29 | } 30 | } 31 | } 32 | } 33 | 34 | internal val FirSession.pokoFirExtensionSessionComponent: PokoFirExtensionSessionComponent by FirSession.sessionComponentAccessor() 35 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/OnlyEqualsAndHashCodeTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import assertk.assertThat 3 | import assertk.assertions.doesNotContain 4 | import assertk.assertions.hashCodeFun 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isNotEqualTo 7 | import data.IdThing 8 | import kotlin.test.Test 9 | import poko.OnlyEqualsAndHashCode 10 | 11 | class OnlyEqualsAndHashCodeTest { 12 | @Test fun two_equivalent_compiled_OnlyEqualsAndHashCode_instances_are_equals() { 13 | val a = OnlyEqualsAndHashCode(id = 1L) 14 | val b = OnlyEqualsAndHashCode(id = 1L) 15 | assertThat(a).isEqualTo(b) 16 | assertThat(b).isEqualTo(a) 17 | assertThat(a).hashCodeFun().isEqualTo(b.hashCode()) 18 | } 19 | 20 | @Test fun two_inequivalent_compiled_OnlyEqualsAndHashCode_instances_are_not_equals() { 21 | val a = OnlyEqualsAndHashCode(id = 2L) 22 | val b = OnlyEqualsAndHashCode(id = 3L) 23 | assertThat(a).isNotEqualTo(b) 24 | assertThat(b).isNotEqualTo(a) 25 | assertThat(a).hashCodeFun().isNotEqualTo(b.hashCode()) 26 | } 27 | 28 | @Test fun onlyEqualsAndHashCode_instance_has_expected_hashCode() { 29 | val poko = OnlyEqualsAndHashCode(id = 4L) 30 | val data = IdThing(id = 4L) 31 | assertThat(poko).hashCodeFun().isEqualTo(data.hashCode()) 32 | } 33 | 34 | @Test fun onlyEqualsAndHashCode_instance_has_generic_toString() { 35 | assertThat(OnlyEqualsAndHashCode(id = 100L).toString()) 36 | .doesNotContain("id", "=", "100") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 17 | 18 | 19 | 21 | 22 | 23 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/irHelpers.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import dev.drewhamilton.poko.PokoAnnotationNames 4 | import org.jetbrains.kotlin.KtFakeSourceElementKind 5 | import org.jetbrains.kotlin.fir.backend.FirMetadataSource 6 | import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI 7 | import org.jetbrains.kotlin.ir.declarations.IrClass 8 | import org.jetbrains.kotlin.ir.declarations.IrProperty 9 | import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI 10 | import org.jetbrains.kotlin.ir.util.hasAnnotation 11 | import org.jetbrains.kotlin.ir.util.properties 12 | import org.jetbrains.kotlin.name.ClassId 13 | import org.jetbrains.kotlin.psi.KtParameter 14 | import org.jetbrains.kotlin.resolve.source.getPsi 15 | 16 | @UnsafeDuringIrConstructionAPI 17 | internal fun IrClass.pokoProperties( 18 | pokoAnnotation: ClassId, 19 | ): List { 20 | return properties 21 | .toList() 22 | .filter { 23 | val metadata = it.metadata 24 | if (metadata is FirMetadataSource.Property) { 25 | // Using K2: 26 | metadata.fir.source?.kind is KtFakeSourceElementKind.PropertyFromParameter 27 | } else { 28 | // Not using K2: 29 | @OptIn(ObsoleteDescriptorBasedAPI::class) 30 | it.symbol.descriptor.source.getPsi() is KtParameter 31 | } 32 | } 33 | .filter { 34 | !it.hasAnnotation( 35 | classId = pokoAnnotation.createNestedClassId(PokoAnnotationNames.Skip), 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/ExplicitDeclarationsTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isNotEqualTo 6 | import assertk.assertions.toStringFun 7 | import kotlin.test.Test 8 | import data.ExplicitDeclarations as ExplicitDeclarationsData 9 | import poko.ExplicitDeclarations as ExplicitDeclarationsPoko 10 | 11 | class ExplicitDeclarationsTest { 12 | @Test fun two_equivalent_compiled_ExplicitDeclarations_instances_are_equals() { 13 | val a = ExplicitDeclarationsPoko("string 1") 14 | val b = ExplicitDeclarationsPoko("string 2") 15 | assertThat(a).isEqualTo(b) 16 | assertThat(b).isEqualTo(a) 17 | } 18 | 19 | @Test fun two_inequivalent_compiled_ExplicitDeclarations_instances_are_not_equals() { 20 | val a = ExplicitDeclarationsPoko("string 1") 21 | val b = ExplicitDeclarationsPoko("string 11") 22 | assertThat(a).isNotEqualTo(b) 23 | assertThat(b).isNotEqualTo(a) 24 | } 25 | 26 | @Test fun compilation_with_explicit_function_declarations_respects_explicit_hashCode() { 27 | val testString = "test string" 28 | val poko = ExplicitDeclarationsPoko(testString) 29 | val data = ExplicitDeclarationsData(testString) 30 | 31 | assertThat(poko).all { 32 | hashCodeFun().all { 33 | isEqualTo(testString.length) 34 | isEqualTo(data.hashCode()) 35 | } 36 | toStringFun().all { 37 | isEqualTo(testString) 38 | isEqualTo(data.toString()) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/PokoCommandLineProcessor.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | import com.google.auto.service.AutoService 4 | import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption 5 | import org.jetbrains.kotlin.compiler.plugin.CliOption 6 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 7 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 8 | import org.jetbrains.kotlin.config.CompilerConfiguration 9 | 10 | @AutoService(CommandLineProcessor::class) 11 | @ExperimentalCompilerApi 12 | public class PokoCommandLineProcessor : CommandLineProcessor { 13 | 14 | override val pluginId: String get() = BuildConfig.COMPILER_PLUGIN_ID 15 | 16 | override val pluginOptions: Collection = listOf( 17 | CliOption( 18 | optionName = CompilerOptions.ENABLED.toString(), 19 | valueDescription = "", 20 | description = "", 21 | required = false, 22 | ), 23 | CliOption( 24 | optionName = CompilerOptions.POKO_ANNOTATION.toString(), 25 | valueDescription = "Annotation class name", 26 | description = "", 27 | required = false, 28 | ), 29 | ) 30 | 31 | override fun processOption( 32 | option: AbstractCliOption, 33 | value: String, 34 | configuration: CompilerConfiguration 35 | ): Unit = when (option.optionName) { 36 | CompilerOptions.ENABLED.toString() -> 37 | configuration.put(CompilerOptions.ENABLED, value.toBoolean()) 38 | CompilerOptions.POKO_ANNOTATION.toString() -> 39 | configuration.put(CompilerOptions.POKO_ANNOTATION, value) 40 | else -> 41 | throw IllegalArgumentException("Unknown plugin option: ${option.optionName}") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/SuperclassDeclarationsTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isNotEqualTo 6 | import assertk.assertions.toStringFun 7 | import kotlin.test.Test 8 | import data.SuperclassDeclarations as SuperclassDeclarationsData 9 | import poko.SuperclassDeclarations as SuperclassDeclarationsPoko 10 | 11 | class SuperclassDeclarationsTest { 12 | @Test fun two_equivalent_compiled_Subclass_instances_are_equals() { 13 | val a = SuperclassDeclarationsPoko(999.9) 14 | val b = SuperclassDeclarationsPoko(999.9) 15 | 16 | // Super class equals implementation returns `other == true`; this confirms that is overridden: 17 | assertThat(a).isNotEqualTo(true) 18 | assertThat(b).isNotEqualTo(true) 19 | 20 | assertThat(a).isEqualTo(b) 21 | assertThat(b).isEqualTo(a) 22 | } 23 | 24 | @Test fun two_inequivalent_compiled_Subclass_instances_are_not_equals() { 25 | val a = SuperclassDeclarationsPoko(999.9) 26 | val b = SuperclassDeclarationsPoko(888.8) 27 | // Super class equals implementation returns `other == true`; this confirms that is overridden: 28 | assertThat(a).isNotEqualTo(true) 29 | assertThat(b).isNotEqualTo(true) 30 | 31 | assertThat(a).isNotEqualTo(b) 32 | assertThat(b).isNotEqualTo(a) 33 | } 34 | 35 | @Test fun superclass_hashCode_is_overridden() { 36 | val poko = SuperclassDeclarationsPoko(123.4) 37 | val data = SuperclassDeclarationsData(123.4) 38 | assertThat(poko).all { 39 | hashCodeFun().all { 40 | isEqualTo(data.hashCode()) 41 | isNotEqualTo(50934) 42 | } 43 | toStringFun().all { 44 | isEqualTo(data.toString()) 45 | isNotEqualTo("superclass") 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/test/kotlin/dev/drewhamilton/poko/gradle/PokoGradlePluginFixtureTest.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.gradle 2 | 3 | import com.google.testing.junit.testparameterinjector.TestParameter 4 | import com.google.testing.junit.testparameterinjector.TestParameterInjector 5 | import dev.drewhamilton.poko.gradle.TestBuildConfig.MINIMUM_GRADLE_VERSION 6 | import java.io.File 7 | import org.gradle.testkit.runner.GradleRunner 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(TestParameterInjector::class) 12 | class PokoGradlePluginFixtureTest( 13 | @param:TestParameter(LATEST_GRADLE_VERSION, MINIMUM_GRADLE_VERSION) 14 | private val gradleVersion: String, 15 | @param:TestParameter 16 | private val isolatedProjects: Boolean, 17 | ) { 18 | @Test fun simple() { 19 | createRunner(File("src/test/fixtures/simple")).build() 20 | } 21 | 22 | private fun createRunner( 23 | fixtureDir: File, 24 | vararg tasks: String = arrayOf("clean", "build") 25 | ): GradleRunner { 26 | return GradleRunner.create() 27 | .apply { 28 | if (gradleVersion != LATEST_GRADLE_VERSION) { 29 | withGradleVersion(gradleVersion) 30 | } 31 | } 32 | .withProjectDir(fixtureDir) 33 | .withDebug(true) // Run in-process. 34 | .withArguments( 35 | *tasks, 36 | "--stacktrace", 37 | VERSION_PROPERTY, 38 | VALIDATE_KOTLIN_METADATA, 39 | "-Dorg.gradle.configuration-cache=true", 40 | "-Dorg.gradle.unsafe.isolated-projects=$isolatedProjects" 41 | ) 42 | .forwardOutput() 43 | } 44 | } 45 | 46 | private const val LATEST_GRADLE_VERSION = "latest" 47 | 48 | private const val VERSION_PROPERTY = "-PpokoVersion=${BuildConfig.VERSION}" 49 | private const val VALIDATE_KOTLIN_METADATA = "-Porg.gradle.kotlin.dsl.skipMetadataVersionCheck=false" 50 | -------------------------------------------------------------------------------- /poko-tests/performance/src/test/kotlin/JvmPerformanceTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertThat 3 | import assertk.assertions.contains 4 | import assertk.assertions.doesNotContain 5 | import org.junit.AssumptionViolatedException 6 | import org.junit.Test 7 | import org.objectweb.asm.ClassReader 8 | 9 | class JvmPerformanceTest { 10 | @Test fun `int property does not emit hashCode method invocation`() { 11 | val classfile = jvmOutput("performance/IntAndLong.class") 12 | val bytecode = bytecodeToText(classfile.readBytes()) 13 | assertThat(bytecode).all { 14 | contains("java/lang/Long.hashCode") 15 | doesNotContain("java/lang/Integer.hashCode") 16 | } 17 | } 18 | 19 | @Test fun `uint property does not emit hashCode method invocation`() { 20 | val classfile = jvmOutput("performance/UIntAndLong.class") 21 | val bytecode = bytecodeToText(classfile.readBytes()) 22 | assertThat(bytecode).all { 23 | contains("java/lang/Long.hashCode") 24 | doesNotContain("kotlin/UInt.hashCode-impl") 25 | } 26 | } 27 | 28 | @Test fun `toString uses invokedynamic on modern JDKs`() { 29 | val classfile = jvmOutput("performance/IntAndLong.class") 30 | val classReader = ClassReader(classfile.readBytes()) 31 | // Java 9 == class file major version 53: 32 | classReader.assumeMinimumClassVersion(53) 33 | val bytecode = classReader.toText() 34 | assertThat(bytecode).all { 35 | contains("INVOKEDYNAMIC makeConcatWithConstants") 36 | doesNotContain("StringBuilder") 37 | } 38 | } 39 | 40 | private fun ClassReader.assumeMinimumClassVersion(version: Int) { 41 | // Class file major version is a two-byte integer at offset 6: 42 | val actualClassVersion = readShort(6) 43 | if (actualClassVersion < version) { 44 | throw AssumptionViolatedException("This test only works class version $version+") 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/PokoIrGenerationExtension.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 6 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 7 | import org.jetbrains.kotlin.cli.common.messages.MessageUtil 8 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 9 | import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid 10 | import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi 11 | import org.jetbrains.kotlin.name.ClassId 12 | 13 | internal class PokoIrGenerationExtension( 14 | private val pokoAnnotationName: ClassId, 15 | private val messageCollector: MessageCollector 16 | ) : IrGenerationExtension { 17 | 18 | override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { 19 | if (pluginContext.referenceClass(pokoAnnotationName) == null) { 20 | moduleFragment.reportError("Could not find class <$pokoAnnotationName>") 21 | return 22 | } 23 | 24 | if (pluginContext.afterK2) { 25 | val bodyFiller = PokoFunctionBodyFiller( 26 | pokoAnnotation = pokoAnnotationName, 27 | context = pluginContext, 28 | messageCollector = messageCollector, 29 | ) 30 | moduleFragment.acceptChildrenVoid(bodyFiller) 31 | } else { 32 | val pokoMembersTransformer = PokoMembersTransformer( 33 | pokoAnnotationName = pokoAnnotationName, 34 | pluginContext = pluginContext, 35 | messageCollector = messageCollector, 36 | ) 37 | moduleFragment.transform(pokoMembersTransformer, null) 38 | } 39 | } 40 | 41 | private fun IrModuleFragment.reportError(message: String) { 42 | val psi = descriptor.findPsi() 43 | val location = MessageUtil.psiElementToMessageLocation(psi) 44 | messageCollector.report(CompilerMessageSeverity.ERROR, message, location) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/PokoCompilerPluginRegistrar.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | import com.google.auto.service.AutoService 4 | import dev.drewhamilton.poko.BuildConfig.DEFAULT_POKO_ANNOTATION 5 | import dev.drewhamilton.poko.BuildConfig.DEFAULT_POKO_ENABLED 6 | import dev.drewhamilton.poko.fir.PokoFirExtensionRegistrar 7 | import dev.drewhamilton.poko.ir.PokoIrGenerationExtension 8 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 9 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 10 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 11 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 12 | import org.jetbrains.kotlin.config.CommonConfigurationKeys 13 | import org.jetbrains.kotlin.config.CompilerConfiguration 14 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter 15 | import org.jetbrains.kotlin.name.ClassId 16 | 17 | @ExperimentalCompilerApi 18 | @AutoService(CompilerPluginRegistrar::class) 19 | public class PokoCompilerPluginRegistrar : CompilerPluginRegistrar() { 20 | 21 | override val pluginId: String get() = BuildConfig.COMPILER_PLUGIN_ID 22 | 23 | override val supportsK2: Boolean get() = true 24 | 25 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 26 | if (!configuration.get(CompilerOptions.ENABLED, DEFAULT_POKO_ENABLED)) 27 | return 28 | 29 | val pokoAnnotationString = configuration.get(CompilerOptions.POKO_ANNOTATION, DEFAULT_POKO_ANNOTATION) 30 | val pokoAnnotationClassId = ClassId.fromString(pokoAnnotationString) 31 | 32 | val messageCollector = configuration.get( 33 | CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, 34 | MessageCollector.NONE, 35 | ) 36 | 37 | IrGenerationExtension.registerExtension( 38 | PokoIrGenerationExtension( 39 | pokoAnnotationName = pokoAnnotationClassId, 40 | messageCollector = messageCollector, 41 | ) 42 | ) 43 | 44 | FirExtensionRegistrarAdapter.registerExtension( 45 | PokoFirExtensionRegistrar( 46 | pokoAnnotation = pokoAnnotationClassId, 47 | ) 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sample/mpp/src/commonTest/kotlin/dev/drewhamilton/poko/sample/mpp/ArraysSampleTest.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.mpp 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import kotlin.test.Test 6 | 7 | class ArraysSampleTest { 8 | 9 | @Test fun equals_works() { 10 | val a = ArraysSample( 11 | primitive = byteArrayOf(0x1F.toByte()), 12 | standard = arrayOf("string 1", "string 2"), 13 | nested = arrayOf(charArrayOf('a'), charArrayOf('b')), 14 | runtime = arrayOf("one", 2), 15 | ) 16 | val b = ArraysSample( 17 | primitive = byteArrayOf(0x1F.toByte()), 18 | standard = arrayOf("string 1", "string 2"), 19 | nested = arrayOf(charArrayOf('a'), charArrayOf('b')), 20 | runtime = arrayOf("one", 2), 21 | ) 22 | 23 | assertThat(a).isEqualTo(b) 24 | assertThat(b).isEqualTo(a) 25 | } 26 | 27 | @Test fun hashCode_is_consistent() { 28 | val a = ArraysSample( 29 | primitive = byteArrayOf(0x1F.toByte()), 30 | standard = arrayOf("string 1", "string 2"), 31 | nested = arrayOf(charArrayOf('a'), charArrayOf('b')), 32 | runtime = arrayOf("one", 2), 33 | ) 34 | val b = ArraysSample( 35 | primitive = byteArrayOf(0x1F.toByte()), 36 | standard = arrayOf("string 1", "string 2"), 37 | nested = arrayOf(charArrayOf('a'), charArrayOf('b')), 38 | runtime = arrayOf("one", 2), 39 | ) 40 | 41 | assertThat(a.hashCode()).isEqualTo(b.hashCode()) 42 | } 43 | 44 | @Test fun toString_includes_class_name_and_each_property() { 45 | val sample = ArraysSample( 46 | primitive = byteArrayOf(0x1F.toByte()), 47 | standard = arrayOf("string 1", "string 2"), 48 | nested = arrayOf(charArrayOf('a'), charArrayOf('b')), 49 | runtime = arrayOf("one", 2), 50 | ) 51 | assertThat(sample.toString()).isEqualTo( 52 | other = "ArraysSample(" + 53 | "primitive=[31], " + 54 | "standard=[string 1, string 2], " + 55 | "nested=[[a], [b]], " + 56 | "runtime=[one, 2])", 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /poko-gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.gmazzo.buildconfig.BuildConfigExtension 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 3 | 4 | plugins { 5 | `java-gradle-plugin` 6 | id("org.jetbrains.kotlin.jvm") 7 | } 8 | 9 | // Keep these in sync with each other. See https://docs.gradle.org/current/userguide/compatibility.html#kotlin. 10 | private val minimumGradleVersion = "9.0.0" 11 | private val minimumGradleKotlinVersion = KotlinVersion.KOTLIN_2_2 12 | private val minimumGradleJavaVersion = 24 13 | 14 | pokoBuild { 15 | publishing("Poko Gradle Plugin") 16 | generateBuildConfig("dev.drewhamilton.poko.gradle") 17 | enableBackwardsCompatibility( 18 | lowestSupportedKotlinVersion = minimumGradleKotlinVersion, 19 | lowestSupportedKotlinJvmVersion = minimumGradleKotlinVersion, 20 | ) 21 | } 22 | 23 | gradlePlugin { 24 | plugins { 25 | create("poko") { 26 | id = "dev.drewhamilton.poko" 27 | implementationClass = "dev.drewhamilton.poko.gradle.PokoGradlePlugin" 28 | } 29 | } 30 | } 31 | 32 | kotlin { 33 | jvmToolchain(minimumGradleJavaVersion) 34 | } 35 | 36 | configurations.apiElements { 37 | attributes { 38 | attribute( 39 | GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, 40 | objects.named(GradlePluginApiVersion::class, minimumGradleVersion), 41 | ) 42 | } 43 | } 44 | 45 | with(the()) { 46 | sourceSets.named("test") { 47 | buildConfigField(String::class.java, "MINIMUM_GRADLE_VERSION", minimumGradleVersion) 48 | } 49 | } 50 | 51 | tasks.validatePlugins { 52 | enableStricterValidation.set(true) 53 | } 54 | 55 | dependencies { 56 | compileOnly(libs.kotlin.gradleApi) 57 | 58 | testImplementation(libs.junit) 59 | testImplementation(libs.assertk) 60 | testImplementation(libs.testParameterInjector) 61 | testImplementation(gradleTestKit()) 62 | } 63 | 64 | tasks.test { 65 | inputs.dir(file("src/test/fixtures")) 66 | dependsOn( 67 | ":poko-annotations:publishAllPublicationsToTestingRepository", 68 | ":poko-compiler-plugin:publishAllPublicationsToTestingRepository", 69 | ":poko-gradle-plugin:publishAllPublicationsToTestingRepository", 70 | ) 71 | jvmArgs( 72 | "--add-opens=java.base/java.util=ALL-UNNAMED", 73 | "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | androidx-compose-runtime = "1.10.0" 4 | 5 | kotlin = "2.3.0" 6 | kotlinCompileTesting = "1.6.0" 7 | # https://central.sonatype.com/artifact/dev.zacsweers.kctfork/core/versions: 8 | kotlinCompileTestingFork = "0.12.0" 9 | # https://github.com/google/ksp/releases: 10 | ksp = "2.3.4" 11 | 12 | [libraries] 13 | 14 | androidx-compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "androidx-compose-runtime" } 15 | 16 | autoService-annotations = { module = "com.google.auto.service:auto-service-annotations", version = "1.1.1" } 17 | autoService-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version = "1.2.0" } 18 | 19 | junit = { module = "junit:junit", version = "4.13.2" } 20 | 21 | kotlinCompileTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlinCompileTesting" } 22 | kotlinCompileTestingFork = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTestingFork" } 23 | 24 | kotlin-embeddableCompiler = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } 25 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 26 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 27 | kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 28 | kotlin-gradleApi = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } 29 | 30 | assertk = "com.willowtreeapps.assertk:assertk:0.28.1" 31 | asm-util = "org.ow2.asm:asm-util:9.9.1" 32 | testParameterInjector = "com.google.testparameterinjector:test-parameter-injector:1.20" 33 | 34 | plugin-buildconfig = "com.github.gmazzo.buildconfig:plugin:6.0.7" 35 | plugin-mavenPublish = "com.vanniktech:gradle-maven-publish-plugin:0.35.0" 36 | plugin-dokka = "org.jetbrains.dokka:dokka-gradle-plugin:2.1.0" 37 | 38 | [plugins] 39 | 40 | android-library = { id = "com.android.library", version = "8.13.2" } 41 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 42 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 43 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 44 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 45 | kotlinx-binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.18.1" } 46 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 47 | -------------------------------------------------------------------------------- /poko-tests/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 3 | import org.jetbrains.kotlin.gradle.plugin.NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME 4 | import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 6 | 7 | plugins { 8 | id("org.jetbrains.kotlin.multiplatform") 9 | } 10 | 11 | val compileMode = findProperty("pokoTests.compileMode") 12 | when (compileMode) { 13 | null -> Unit // Nothing to configure 14 | 15 | "WITHOUT_K2" -> { 16 | logger.lifecycle("Building :poko-tests without K2 (language level 1.9)") 17 | tasks.withType().configureEach { 18 | compilerOptions { 19 | languageVersion = KotlinVersion.KOTLIN_1_9 20 | progressiveMode = false 21 | } 22 | } 23 | } 24 | 25 | else -> throw IllegalArgumentException("Unknown pokoTests.compileMode: <$compileMode>") 26 | } 27 | 28 | val jvmToolchainVersion = (findProperty("pokoTests.jvmToolchainVersion") as? String)?.toInt() 29 | 30 | kotlin { 31 | compilerOptions { 32 | freeCompilerArgs.add("-Xexpect-actual-classes") 33 | } 34 | 35 | jvmToolchainVersion?.let { jvmToolchain(it) } 36 | 37 | jvm() 38 | 39 | js { 40 | nodejs() 41 | // Produce a JS file for performance tests. 42 | binaries.executable() 43 | } 44 | 45 | mingwX64() 46 | 47 | linuxArm64() 48 | linuxX64() 49 | 50 | iosArm64() 51 | iosSimulatorArm64() 52 | iosX64() 53 | 54 | macosArm64() 55 | macosX64() 56 | 57 | tvosArm64() 58 | tvosX64() 59 | tvosSimulatorArm64() 60 | 61 | @OptIn(ExperimentalWasmDsl::class) 62 | wasmJs().nodejs() 63 | @OptIn(ExperimentalWasmDsl::class) 64 | wasmWasi().nodejs() 65 | 66 | watchosArm32() 67 | watchosArm64() 68 | watchosDeviceArm64() 69 | watchosSimulatorArm64() 70 | watchosX64() 71 | 72 | androidNativeArm32() 73 | androidNativeArm64() 74 | 75 | androidNativeX86() 76 | androidNativeX64() 77 | 78 | sourceSets { 79 | commonMain { 80 | dependencies { 81 | implementation(project(":poko-annotations")) 82 | } 83 | } 84 | commonTest { 85 | dependencies { 86 | implementation(libs.kotlin.test) 87 | implementation(libs.assertk) 88 | } 89 | } 90 | } 91 | } 92 | 93 | dependencies { 94 | add(PLUGIN_CLASSPATH_CONFIGURATION_NAME, project(":poko-compiler-plugin")) 95 | add(NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME, project(":poko-compiler-plugin")) 96 | } 97 | -------------------------------------------------------------------------------- /sample/compose/src/test/kotlin/dev/drewhamilton/poko/sample/compose/SampleTest.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.compose 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import org.junit.Test 6 | 7 | class SampleTest { 8 | 9 | @Test fun `equals works`() { 10 | val a = Sample( 11 | int = 1, 12 | requiredString = "String", 13 | optionalString = null 14 | ) 15 | val b = Sample( 16 | int = 1, 17 | requiredString = "String", 18 | optionalString = null 19 | ) 20 | 21 | assertThat(a).isEqualTo(b) 22 | assertThat(b).isEqualTo(a) 23 | } 24 | 25 | @Test fun `hashCode is consistent`() { 26 | val a = Sample( 27 | int = 1, 28 | requiredString = "String", 29 | optionalString = null 30 | ) 31 | val b = Sample( 32 | int = 1, 33 | requiredString = "String", 34 | optionalString = null 35 | ) 36 | 37 | assertThat(a.hashCode()).isEqualTo(b.hashCode()) 38 | } 39 | 40 | @Test fun `hashCode is equivalent to data class hashCode`() { 41 | val dataApi = Sample( 42 | int = 1, 43 | requiredString = "String", 44 | optionalString = null 45 | ) 46 | 47 | val data = DataSample( 48 | int = 1, 49 | requiredString = "String", 50 | optionalString = null 51 | ) 52 | 53 | assertThat(dataApi.hashCode()).isEqualTo(data.hashCode()) 54 | } 55 | 56 | @Test fun `toString includes class name and each property`() { 57 | val sample = Sample(3, "sample", null) 58 | assertThat(sample.toString()).isEqualTo("Sample(int=3, requiredString=sample, optionalString=null)") 59 | } 60 | 61 | @Test fun `toString is equivalent to data class toString`() { 62 | val dataApi = Sample( 63 | int = 99, 64 | requiredString = "test", 65 | optionalString = null 66 | ) 67 | 68 | val data = DataSample( 69 | int = 99, 70 | requiredString = "test", 71 | optionalString = null 72 | ) 73 | 74 | assertThat(dataApi.toString()).isEqualTo(data.toString().removePrefix("Data")) 75 | } 76 | 77 | /** 78 | * Data class equivalent to [Sample]. 79 | */ 80 | private data class DataSample( 81 | val int: Int, 82 | val requiredString: String, 83 | val optionalString: String? 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /sample/mpp/src/commonTest/kotlin/dev/drewhamilton/poko/sample/mpp/SampleTest.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.mpp 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import kotlin.test.Test 6 | 7 | class SampleTest { 8 | 9 | @Test fun equals_works() { 10 | val a = Sample( 11 | int = 1, 12 | requiredString = "String", 13 | optionalString = null 14 | ) 15 | val b = Sample( 16 | int = 1, 17 | requiredString = "String", 18 | optionalString = null 19 | ) 20 | 21 | assertThat(a).isEqualTo(b) 22 | assertThat(b).isEqualTo(a) 23 | } 24 | 25 | @Test fun hashCode_is_consistent() { 26 | val a = Sample( 27 | int = 1, 28 | requiredString = "String", 29 | optionalString = null 30 | ) 31 | val b = Sample( 32 | int = 1, 33 | requiredString = "String", 34 | optionalString = null 35 | ) 36 | 37 | assertThat(a.hashCode()).isEqualTo(b.hashCode()) 38 | } 39 | 40 | @Test fun hashCode_is_equivalent_to_data_class_hashCode() { 41 | val dataApi = Sample( 42 | int = 1, 43 | requiredString = "String", 44 | optionalString = null 45 | ) 46 | 47 | val data = DataSample( 48 | int = 1, 49 | requiredString = "String", 50 | optionalString = null 51 | ) 52 | 53 | assertThat(dataApi.hashCode()).isEqualTo(data.hashCode()) 54 | } 55 | 56 | @Test fun toString_includes_class_name_and_each_property() { 57 | val sample = Sample(3, "sample", null) 58 | assertThat(sample.toString()) 59 | .isEqualTo("Sample(int=3, requiredString=sample, optionalString=null)") 60 | } 61 | 62 | @Test fun toString_is_equivalent_to_data_class_toString() { 63 | val dataApi = Sample( 64 | int = 99, 65 | requiredString = "test", 66 | optionalString = null 67 | ) 68 | 69 | val data = DataSample( 70 | int = 99, 71 | requiredString = "test", 72 | optionalString = null 73 | ) 74 | 75 | assertThat(dataApi.toString()).isEqualTo(data.toString().removePrefix("Data")) 76 | } 77 | 78 | /** 79 | * Data class equivalent to [Sample]. 80 | */ 81 | private data class DataSample( 82 | val int: Int, 83 | val requiredString: String, 84 | val optionalString: String? 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /sample/jvm/src/test/kotlin/dev/drewhamilton/poko/sample/jvm/SampleTest.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.sample.jvm 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import org.junit.Test 6 | 7 | class SampleTest { 8 | 9 | @Test fun `equals works`() { 10 | val a = Sample( 11 | int = 1, 12 | requiredString = "String", 13 | optionalString = null 14 | ) 15 | val b = Sample( 16 | int = 1, 17 | requiredString = "String", 18 | optionalString = null 19 | ) 20 | 21 | assertThat(a).isEqualTo(b) 22 | assertThat(b).isEqualTo(a) 23 | } 24 | 25 | @Test fun `hashCode is consistent`() { 26 | val a = Sample( 27 | int = 1, 28 | requiredString = "String", 29 | optionalString = null 30 | ) 31 | val b = Sample( 32 | int = 1, 33 | requiredString = "String", 34 | optionalString = null 35 | ) 36 | 37 | assertThat(a.hashCode()).isEqualTo(b.hashCode()) 38 | } 39 | 40 | @Test fun `hashCode is equivalent to data class hashCode`() { 41 | val dataApi = Sample( 42 | int = 1, 43 | requiredString = "String", 44 | optionalString = null 45 | ) 46 | 47 | val data = DataSample( 48 | int = 1, 49 | requiredString = "String", 50 | optionalString = null 51 | ) 52 | 53 | assertThat(dataApi.hashCode()).isEqualTo(data.hashCode()) 54 | } 55 | 56 | @Test fun `toString includes class name and each property`() { 57 | val sample = Sample(3, "sample", null) 58 | assertThat(sample.toString()) 59 | .isEqualTo("Sample(int=3, requiredString=sample, optionalString=null)") 60 | } 61 | 62 | @Test fun `toString is equivalent to data class toString`() { 63 | val dataApi = Sample( 64 | int = 99, 65 | requiredString = "test", 66 | optionalString = null 67 | ) 68 | 69 | val data = DataSample( 70 | int = 99, 71 | requiredString = "test", 72 | optionalString = null 73 | ) 74 | 75 | assertThat(dataApi.toString()).isEqualTo(data.toString().removePrefix("Data")) 76 | } 77 | 78 | /** 79 | * Data class equivalent to [Sample]. 80 | */ 81 | private data class DataSample( 82 | val int: Int, 83 | val requiredString: String, 84 | val optionalString: String? 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import dev.drewhamilton.poko.sample.build.kotlinJvmTarget 3 | import dev.drewhamilton.poko.sample.build.resolvedJavaVersion 4 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions 5 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 6 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask 7 | 8 | plugins { 9 | alias(libs.plugins.android.library) apply false 10 | alias(libs.plugins.kotlin.android) apply false 11 | alias(libs.plugins.kotlin.jvm) apply false 12 | alias(libs.plugins.kotlin.multiplatform) apply false 13 | id("dev.drewhamilton.poko") apply false 14 | } 15 | apply(from = "properties.gradle") 16 | 17 | logger.lifecycle("Compiling sample app with Kotlin ${libs.versions.kotlin.get()}") 18 | logger.lifecycle("Targeting Java version $resolvedJavaVersion") 19 | 20 | val specifiedKotlinLanguageVersion = findProperty("pokoSample_kotlinLanguageVersion") 21 | ?.toString() 22 | ?.let { it.ifBlank { null } } 23 | ?.let { KotlinVersion.fromVersion(it) } 24 | ?.also { logger.lifecycle("Compiling sample project with language version $it") } 25 | 26 | allprojects { 27 | repositories { 28 | if (System.getenv()["CI"] == "true") { 29 | logger.lifecycle("Resolving ${this@allprojects} Poko dependencies from MavenLocal") 30 | exclusiveContent { 31 | forRepository { mavenLocal() } 32 | filter { 33 | includeGroup(property("PUBLISH_GROUP") as String) 34 | } 35 | } 36 | } 37 | mavenCentral() 38 | 39 | val kotlinDevRepository = rootProject.findProperty("kotlin_dev_repository") 40 | if (kotlinDevRepository != null) { 41 | logger.lifecycle("Adding <$kotlinDevRepository> repository for ${this@allprojects}") 42 | maven { url = uri(kotlinDevRepository) } 43 | } 44 | } 45 | 46 | listOf( 47 | "org.jetbrains.kotlin.jvm", 48 | "org.jetbrains.kotlin.multiplatform", 49 | ).forEach { id -> 50 | plugins.withId(id) { 51 | with(extensions.getByType()) { 52 | sourceCompatibility = resolvedJavaVersion 53 | targetCompatibility = resolvedJavaVersion 54 | } 55 | } 56 | } 57 | 58 | tasks.withType>().configureEach { 59 | compilerOptions { 60 | if (this is KotlinJvmCompilerOptions) { 61 | jvmTarget = kotlinJvmTarget 62 | } 63 | languageVersion = specifiedKotlinLanguageVersion 64 | progressiveMode = (specifiedKotlinLanguageVersion == null) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/GenericArrayHolderTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import assertk.assertThat 3 | import assertk.assertions.hashCodeFun 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isNotEqualTo 6 | import assertk.assertions.toStringFun 7 | import kotlin.test.Test 8 | import poko.GenericArrayHolder 9 | 10 | class GenericArrayHolderTest { 11 | @Test fun two_GenericArrayHolder_instances_with_equivalent_typed_arrays_are_equals() { 12 | val a = GenericArrayHolder( 13 | generic = arrayOf( 14 | arrayOf("5%, 10%"), 15 | intArrayOf(5, 10), 16 | booleanArrayOf(false, true), 17 | Unit, 18 | ), 19 | ) 20 | val b = GenericArrayHolder( 21 | generic = arrayOf( 22 | arrayOf("5%, 10%"), 23 | intArrayOf(5, 10), 24 | booleanArrayOf(false, true), 25 | Unit, 26 | ), 27 | ) 28 | 29 | assertThat(a).isEqualTo(b) 30 | assertThat(b).isEqualTo(a) 31 | assertThat(a).hashCodeFun().isEqualTo(b.hashCode()) 32 | assertThat(a).toStringFun().isEqualTo(b.toString()) 33 | } 34 | 35 | @Test fun two_GenericArrayHolder_instances_with_equivalent_int_arrays_are_equals() { 36 | val a = GenericArrayHolder(intArrayOf(5, 10)) 37 | val b = GenericArrayHolder(intArrayOf(5, 10)) 38 | assertThat(a).isEqualTo(b) 39 | assertThat(b).isEqualTo(a) 40 | assertThat(a).hashCodeFun().isEqualTo(b.hashCode()) 41 | assertThat(a).toStringFun().isEqualTo(b.toString()) 42 | } 43 | 44 | @Test fun two_GenericArrayHolder_instances_with_equivalent_nonarrays_are_equals() { 45 | val a = GenericArrayHolder("5, 10") 46 | val b = GenericArrayHolder("5, 10") 47 | assertThat(a).isEqualTo(b) 48 | assertThat(b).isEqualTo(a) 49 | assertThat(a).hashCodeFun().isEqualTo(b.hashCode()) 50 | assertThat(a).toStringFun().isEqualTo(b.toString()) 51 | } 52 | 53 | @Test fun two_GenericArrayHolder_instances_holding_inequivalent_long_arrays_are_not_equals() { 54 | val a = GenericArrayHolder(longArrayOf(Long.MIN_VALUE)) 55 | val b = GenericArrayHolder(longArrayOf(Long.MAX_VALUE)) 56 | assertThat(a).isNotEqualTo(b) 57 | assertThat(b).isNotEqualTo(a) 58 | } 59 | 60 | @Test fun two_GenericArrayHolder_instances_holding_mismatching_types_are_not_equals() { 61 | val a = GenericArrayHolder(arrayOf("x", "y")) 62 | val b = GenericArrayHolder("xy") 63 | assertThat(a).isNotEqualTo(b) 64 | assertThat(b).isNotEqualTo(a) 65 | } 66 | 67 | @Test fun hashCode_produces_expected_value() { 68 | val value = GenericArrayHolder( 69 | generic = intArrayOf(50, 100), 70 | ) 71 | // Ensure consistency across platforms: 72 | assertThat(value).hashCodeFun().isEqualTo(2611) 73 | } 74 | 75 | @Test fun toString_produces_expected_value() { 76 | val value = GenericArrayHolder( 77 | generic = intArrayOf(50, 100), 78 | ) 79 | assertThat(value).toStringFun().isEqualTo("GenericArrayHolder(generic=[50, 100])") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /sample/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /poko-gradle-plugin/src/main/kotlin/dev/drewhamilton/poko/gradle/PokoGradlePlugin.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.gradle 2 | 3 | import dev.drewhamilton.poko.gradle.BuildConfig.DEFAULT_POKO_ANNOTATION 4 | import org.gradle.api.Project 5 | import org.gradle.api.plugins.JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME 6 | import org.gradle.api.provider.Provider 7 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 8 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin 9 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet.Companion.COMMON_MAIN_SOURCE_SET_NAME 10 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetContainer 11 | import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact 12 | import org.jetbrains.kotlin.gradle.plugin.SubpluginOption 13 | 14 | public class PokoGradlePlugin : KotlinCompilerPluginSupportPlugin { 15 | 16 | override fun apply(target: Project) { 17 | val extension = target.extensions.create("poko", PokoPluginExtension::class.java) 18 | 19 | target.afterEvaluate { 20 | val annotationDependency = when (extension.pokoAnnotation.get()) { 21 | DEFAULT_POKO_ANNOTATION -> BuildConfig.annotationsDependency 22 | else -> null 23 | } 24 | if (annotationDependency != null) { 25 | if (target.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { 26 | val kotlin = target.extensions.getByName("kotlin") as KotlinSourceSetContainer 27 | kotlin.sourceSets.getByName(COMMON_MAIN_SOURCE_SET_NAME) { sourceSet -> 28 | sourceSet.dependencies { 29 | implementation(annotationDependency) 30 | } 31 | } 32 | } else { 33 | if (target.plugins.hasPlugin("org.gradle.java-test-fixtures")) { 34 | target.dependencies.add("testFixturesImplementation", annotationDependency) 35 | } 36 | target.dependencies.add(IMPLEMENTATION_CONFIGURATION_NAME, annotationDependency) 37 | } 38 | } 39 | } 40 | } 41 | 42 | override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true 43 | 44 | override fun getCompilerPluginId(): String = BuildConfig.COMPILER_PLUGIN_ID 45 | 46 | override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact( 47 | groupId = BuildConfig.GROUP, 48 | artifactId = BuildConfig.COMPILER_PLUGIN_ARTIFACT, 49 | version = BuildConfig.VERSION 50 | ) 51 | 52 | override fun applyToCompilation( 53 | kotlinCompilation: KotlinCompilation<*>, 54 | ): Provider> { 55 | val project = kotlinCompilation.target.project 56 | val extension = project.extensions.getByType(PokoPluginExtension::class.java) 57 | 58 | return project.provider { 59 | listOfNotNull( 60 | SubpluginOption( 61 | key = BuildConfig.POKO_ENABLED_OPTION_NAME, 62 | value = extension.enabled.get().toString(), 63 | ), 64 | SubpluginOption( 65 | key = BuildConfig.POKO_ANNOTATION_OPTION_NAME, 66 | value = extension.pokoAnnotation.get(), 67 | ), 68 | ) 69 | } 70 | } 71 | 72 | private val BuildConfig.annotationsDependency: String 73 | get() = "$GROUP:$ANNOTATIONS_ARTIFACT:$VERSION" 74 | } 75 | -------------------------------------------------------------------------------- /sample/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val isCi = System.getenv()["CI"] == "true" 3 | 4 | apply(from = "properties.gradle") 5 | 6 | if (!isCi) { 7 | includeBuild("../.") 8 | } 9 | 10 | repositories { 11 | if (isCi) { 12 | logger.lifecycle("Resolving buildscript Poko dependencies from MavenLocal") 13 | exclusiveContent { 14 | forRepository { mavenLocal() } 15 | filter { 16 | val publishGroup = extra["PUBLISH_GROUP"] as String 17 | includeGroup(publishGroup) 18 | } 19 | } 20 | } 21 | mavenCentral() 22 | google() 23 | 24 | if (extra.has("kotlin_dev_repository")) { 25 | val kotlinDevRepository = extra["kotlin_dev_repository"]!! 26 | logger.lifecycle("Adding <$kotlinDevRepository> repository for plugins") 27 | maven { url = uri(kotlinDevRepository) } 28 | } 29 | } 30 | 31 | resolutionStrategy { 32 | val publishVersion = extra["PUBLISH_VERSION"] as String 33 | eachPlugin { 34 | if (requested.id.id == "dev.drewhamilton.poko") { 35 | useVersion(publishVersion) 36 | } 37 | } 38 | } 39 | } 40 | 41 | rootProject.name = "PokoSample" 42 | 43 | include(":jvm") 44 | 45 | // Kotlin 2.3 does not support language target 1.9 for multiplatform: 46 | private val ciKotlinLanguageVersion = System.getenv()["ORG_GRADLE_PROJECT_pokoSample_kotlinLanguageVersion"] 47 | if (ciKotlinLanguageVersion?.startsWith("1") == true) { 48 | logger.lifecycle("Language version $ciKotlinLanguageVersion; skipping :mpp module") 49 | } else { 50 | include(":mpp") 51 | } 52 | 53 | include(":compose") 54 | 55 | private val isCi = System.getenv()["CI"] == "true" 56 | if (!isCi) { 57 | // Use local Poko modules for non-CI builds: 58 | includeBuild("../.") { 59 | logger.lifecycle("Replacing Poko module dependencies with local projects") 60 | val publishGroup: String = extra["PUBLISH_GROUP"] as String 61 | dependencySubstitution { 62 | substitute(module("$publishGroup:${extra["poko-annotations.POM_ARTIFACT_ID"]}")) 63 | .using(project(":poko-annotations")) 64 | .because("Developers can see local changes reflected in the sample project") 65 | substitute(module("$publishGroup:${extra["poko-compiler-plugin.POM_ARTIFACT_ID"]}")) 66 | .using(project(":poko-compiler-plugin")) 67 | .because("Developers can see local changes reflected in the sample project") 68 | substitute(module("$publishGroup:${extra["poko-gradle-plugin.POM_ARTIFACT_ID"]}")) 69 | .using(project(":poko-gradle-plugin")) 70 | .because("Developers can see local changes reflected in the sample project") 71 | } 72 | } 73 | } 74 | 75 | dependencyResolutionManagement { 76 | versionCatalogs { 77 | create("libs") { 78 | from(files("../gradle/libs.versions.toml")) 79 | 80 | //region Duplicated in buildSrc/settings.gradle 81 | fun String.nullIfBlank(): String? = if (isNullOrBlank()) null else this 82 | 83 | // Compile sample project with different Kotlin version than Poko, if provided: 84 | val kotlinVersionOverride = System.getenv()["poko_sample_kotlin_version"]?.nullIfBlank() 85 | kotlinVersionOverride?.let { kotlinVersion -> 86 | version("kotlin", kotlinVersion) 87 | } 88 | //endregion 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /poko-tests/src/commonMain/kotlin/data/AnyArrayHolder.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | /** 4 | * Data classes don't implement array content checks, so [equals], [hashCode], and [toString] must 5 | * be written by hand. 6 | */ 7 | @Suppress("Unused") 8 | data class AnyArrayHolder( 9 | val any: Any, 10 | val nullableAny: Any?, 11 | val trailingProperty: String, 12 | ) { 13 | override fun equals(other: Any?): Boolean { 14 | if (this === other) 15 | return true 16 | 17 | if (other !is AnyArrayHolder) 18 | return false 19 | 20 | if (!this.any.arrayContentDeepEquals(other.any)) 21 | return false 22 | 23 | if (!this.nullableAny.arrayContentDeepEquals(other.nullableAny)) 24 | return false 25 | 26 | if (this.trailingProperty != other.trailingProperty) 27 | return false 28 | 29 | return true 30 | } 31 | 32 | @Suppress("NOTHING_TO_INLINE") 33 | private inline fun Any?.arrayContentDeepEquals(other: Any?): Boolean { 34 | return when (this) { 35 | is Array<*> -> other is Array<*> && this.contentDeepEquals(other) 36 | is BooleanArray -> other is BooleanArray && this.contentEquals(other) 37 | is ByteArray -> other is ByteArray && this.contentEquals(other) 38 | is CharArray -> other is CharArray && this.contentEquals(other) 39 | is ShortArray -> other is ShortArray && this.contentEquals(other) 40 | is IntArray -> other is IntArray && this.contentEquals(other) 41 | is LongArray -> other is LongArray && this.contentEquals(other) 42 | is FloatArray -> other is FloatArray && this.contentEquals(other) 43 | is DoubleArray -> other is DoubleArray && this.contentEquals(other) 44 | else -> this == other 45 | } 46 | } 47 | 48 | override fun hashCode(): Int { 49 | var result = any.arrayContentDeepHashCode() 50 | 51 | result = result * 31 + nullableAny.arrayContentDeepHashCode() 52 | result = result * 31 + trailingProperty.hashCode() 53 | 54 | return result 55 | } 56 | 57 | @Suppress("NOTHING_TO_INLINE") 58 | private inline fun Any?.arrayContentDeepHashCode(): Int { 59 | return when (this) { 60 | is Array<*> -> this.contentDeepHashCode() 61 | is BooleanArray -> this.contentHashCode() 62 | is ByteArray -> this.contentHashCode() 63 | is CharArray -> this.contentHashCode() 64 | is ShortArray -> this.contentHashCode() 65 | is IntArray -> this.contentHashCode() 66 | is LongArray -> this.contentHashCode() 67 | is FloatArray -> this.contentHashCode() 68 | is DoubleArray -> this.contentHashCode() 69 | else -> this.hashCode() 70 | } 71 | } 72 | 73 | override fun toString(): String { 74 | return StringBuilder() 75 | .append("AnyArrayHolder(") 76 | .append("any=") 77 | .append(any.arrayContentDeepToString()) 78 | .append(", ") 79 | .append("nullableAny=") 80 | .append(nullableAny.arrayContentDeepToString()) 81 | .append(", ") 82 | .append("trailingProperty=") 83 | .append(trailingProperty) 84 | .append(")") 85 | .toString() 86 | } 87 | 88 | @Suppress("NOTHING_TO_INLINE") 89 | private inline fun Any?.arrayContentDeepToString(): String { 90 | return when (this) { 91 | is Array<*> -> this.contentDeepToString() 92 | is BooleanArray -> this.contentToString() 93 | is ByteArray -> this.contentToString() 94 | is CharArray -> this.contentToString() 95 | is ShortArray -> this.contentToString() 96 | is IntArray -> this.contentToString() 97 | is LongArray -> this.contentToString() 98 | is FloatArray -> this.contentToString() 99 | is DoubleArray -> this.contentToString() 100 | else -> this.toString() 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /poko-annotations/src/commonMain/kotlin/dev/drewhamilton/poko/Poko.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko 2 | 3 | /** 4 | * A `@Poko class` is similar to a `data class`: the Poko compiler plugin will generate [equals], 5 | * [hashCode], and [toString] functions for any Kotlin class marked with this annotation. Unlike 6 | * normal data classes, `copy` or `componentN` functions are not generated. This makes it easier to 7 | * maintain data models in libraries without breaking binary compatibility. 8 | * 9 | * The generated functions will be based on class properties in the primary constructor. Class 10 | * properties not in the primary constructor will not be included, and primary constructor 11 | * parameters that are not class properties will not be included. Compilation will fail if the 12 | * annotated class does not include a primary constructor. 13 | * 14 | * Each function will only be generated if it is not already manually overridden in the annotated 15 | * class. 16 | * 17 | * The annotated class cannot be a `data class`, an `inline class`, or an `inner class`. 18 | * 19 | * Like data classes, it is highly recommended that all properties used in equals/hashCode are 20 | * immutable. `var`s, mutable collections, and especially arrays should be avoided. The class itself 21 | * should also be final. The compiler plugin does not enforce these recommendations. 22 | */ 23 | @Retention(AnnotationRetention.SOURCE) 24 | @Target(AnnotationTarget.CLASS) 25 | public annotation class Poko { 26 | 27 | /** 28 | * Generates the [equals] and [hashCode] functions of a class without generating the `toString` 29 | * function. See further documentation on the [Poko] annotation. 30 | */ 31 | @IndependentFunctionsSupport 32 | @Retention(AnnotationRetention.SOURCE) 33 | @Target(AnnotationTarget.CLASS) 34 | public annotation class EqualsAndHashCode 35 | 36 | /** 37 | * Generates the [toString] function of a class without generating the `equals` or `hashCode` 38 | * functions. See further documentation on the [Poko] annotation. 39 | */ 40 | @IndependentFunctionsSupport 41 | @Retention(AnnotationRetention.SOURCE) 42 | @Target(AnnotationTarget.CLASS) 43 | public annotation class ToString 44 | 45 | /** 46 | * Primary constructor properties marked with this annotation will be omitted from generated 47 | * `equals`, `hashCode`, and `toString` functions, as if they were not properties. 48 | * 49 | * This annotation has no effect on properties declared outside the primary constructor. 50 | */ 51 | @Retention(AnnotationRetention.SOURCE) 52 | @Target(AnnotationTarget.PROPERTY) 53 | public annotation class Skip 54 | 55 | /** 56 | * Declares that a [Poko] class's generated functions will be based on this property's array 57 | * content. This differs from the Poko class (and data class) default of comparing arrays by 58 | * reference only. 59 | * 60 | * Poko class properties of type [Array], [BooleanArray], [CharArray], [ByteArray], [ShortArray], 61 | * [IntArray], [LongArray], [FloatArray], and [DoubleArray] are supported, including nested 62 | * [Array] types. 63 | * 64 | * Properties of a generic type or of type [Any] are also supported. For these properties, Poko will 65 | * generate a `when` statement that disambiguates the various array types at runtime and analyzes 66 | * content if the property is an array. (Note that with this logic, typed arrays will never be 67 | * considered equals to primitive arrays, even if they hold the same content. For example, 68 | * `arrayOf(1, 2)` will not be considered equals to `intArrayOf(1, 2)`.) 69 | * 70 | * Properties of a value class type that wraps an array are not supported. Tagging non-array 71 | * properties with this annotation is an error. 72 | * 73 | * Using array properties in data models is not generally recommended, because they are mutable. 74 | * Mutating an array marked with this annotation will cause the parent Poko class to produce 75 | * different `equals` and `hashCode` results at different times. This annotation should only be used 76 | * by consumers for whom performant code is more important than safe code. 77 | */ 78 | @Retention(AnnotationRetention.SOURCE) 79 | @Target(AnnotationTarget.PROPERTY) 80 | public annotation class ReadArrayContent 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v6 14 | 15 | - name: Install JDK 16 | uses: actions/setup-java@v5 17 | with: 18 | distribution: zulu 19 | java-version: | 20 | 24 21 | 25 22 | 23 | - name: Set up Gradle 24 | uses: gradle/actions/setup-gradle@v5 25 | 26 | - name: Build 27 | run: >- 28 | ./gradlew 29 | :poko-annotations:build 30 | :poko-compiler-plugin:build 31 | :poko-gradle-plugin:build 32 | publishToMavenLocal 33 | --stacktrace 34 | env: 35 | ORG_GRADLE_PROJECT_personalGpgKey: ${{ secrets.ORG_GRADLE_PROJECT_personalGpgKey }} 36 | ORG_GRADLE_PROJECT_personalGpgPassword: ${{ secrets.ORG_GRADLE_PROJECT_personalGpgPassword }} 37 | 38 | - name: Upload MavenLocal directory 39 | uses: actions/upload-artifact@v6 40 | with: 41 | name: MavenLocal 42 | path: ~/.m2/repository/dev/drewhamilton/poko/ 43 | if-no-files-found: error 44 | 45 | - name: Test 46 | # Builds and run tests for any not-yet-built modules, i.e. the :poko-tests modules 47 | run: ./gradlew build --stacktrace 48 | 49 | - name: Test without K2 50 | # Builds and run tests for any not-yet-built modules, i.e. the :poko-tests modules 51 | run: >- 52 | ./gradlew :poko-tests:clean build --stacktrace 53 | -Dorg.gradle.project.pokoTests.compileMode=WITHOUT_K2 54 | 55 | test-with-jdk: 56 | runs-on: ubuntu-latest 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | poko_tests_jvm_toolchain_version: [ 8, 11, 17, 21 ] 61 | steps: 62 | - name: Check out the repo 63 | uses: actions/checkout@v6 64 | 65 | - name: Install JDK ${{ matrix.poko_tests_jvm_toolchain_version }} 66 | uses: actions/setup-java@v5 67 | with: 68 | distribution: zulu 69 | java-version: | 70 | ${{ matrix.poko_tests_jvm_toolchain_version }} 71 | 25 72 | 73 | - name: Set up Gradle 74 | uses: gradle/actions/setup-gradle@v5 75 | 76 | - name: Test 77 | run: >- 78 | ./gradlew :poko-tests:jvmTest --stacktrace 79 | -Dorg.gradle.project.pokoTests.jvmToolchainVersion=${{ matrix.poko_tests_jvm_toolchain_version }} 80 | 81 | - name: Test without K2 82 | run: >- 83 | ./gradlew :poko-tests:jvmTest --stacktrace 84 | -Dorg.gradle.project.pokoTests.jvmToolchainVersion=${{ matrix.poko_tests_jvm_toolchain_version }} 85 | -Dorg.gradle.project.pokoTests.compileMode=WITHOUT_K2 86 | 87 | build-sample: 88 | runs-on: ubuntu-latest 89 | needs: build 90 | strategy: 91 | fail-fast: false 92 | matrix: 93 | poko_sample_kotlin_version: [ ~ ] 94 | poko_sample_kotlin_language_version: [ 1.9, ~ ] 95 | env: 96 | poko_sample_kotlin_version: ${{ matrix.poko_sample_kotlin_version }} 97 | ORG_GRADLE_PROJECT_pokoSample_kotlinLanguageVersion: ${{ matrix.poko_sample_kotlin_language_version }} 98 | steps: 99 | - name: Check out the repo 100 | uses: actions/checkout@v6 101 | - name: Install JDK 102 | uses: actions/setup-java@v5 103 | with: 104 | distribution: zulu 105 | java-version: 25 106 | - name: Download MavenLocal 107 | uses: actions/download-artifact@v7 108 | with: 109 | name: MavenLocal 110 | path: ~/.m2/repository/dev/drewhamilton/poko/ 111 | 112 | - name: Set up Gradle 113 | uses: gradle/actions/setup-gradle@v5 114 | 115 | - name: Upgrade yarn lock 116 | if: ${{ matrix.poko_sample_kotlin_version != null && !(startsWith(matrix.poko_sample_kotlin_version, '2.3') && startsWith(matrix.poko_sample_kotlin_language_version, '1')) }} 117 | run: cd sample && ./gradlew kotlinUpgradeYarnLock 118 | 119 | - name: Build sample 120 | run: cd sample && ./gradlew build --stacktrace 121 | 122 | env: 123 | GRADLE_OPTS: >- 124 | -Dorg.gradle.configureondemand=true 125 | -Dkotlin.incremental=false 126 | -Dorg.gradle.jvmargs="-Xmx3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=512m" 127 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/PokoFunctionBodyFiller.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import dev.drewhamilton.poko.PokoFunction 4 | import dev.drewhamilton.poko.fir.PokoKey 5 | import org.jetbrains.kotlin.GeneratedDeclarationKey 6 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 7 | import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder 8 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 9 | import org.jetbrains.kotlin.ir.IrElement 10 | import org.jetbrains.kotlin.ir.builders.irBlockBody 11 | import org.jetbrains.kotlin.ir.declarations.IrDeclaration 12 | import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin.GeneratedByPlugin 13 | import org.jetbrains.kotlin.ir.declarations.IrFile 14 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 15 | import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction 16 | import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI 17 | import org.jetbrains.kotlin.ir.util.isEquals 18 | import org.jetbrains.kotlin.ir.util.isHashCode 19 | import org.jetbrains.kotlin.ir.util.isToString 20 | import org.jetbrains.kotlin.ir.util.parentAsClass 21 | import org.jetbrains.kotlin.ir.visitors.IrVisitorVoid 22 | import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid 23 | import org.jetbrains.kotlin.name.ClassId 24 | 25 | @OptIn(UnsafeDuringIrConstructionAPI::class) 26 | internal class PokoFunctionBodyFiller( 27 | private val pokoAnnotation: ClassId, 28 | private val context: IrPluginContext, 29 | private val messageCollector: MessageCollector, 30 | ) : IrVisitorVoid() { 31 | 32 | override fun visitSimpleFunction(declaration: IrSimpleFunction) { 33 | val origin = declaration.origin 34 | if (origin !is GeneratedByPlugin || !interestedIn(origin.pluginKey)) { 35 | return 36 | } 37 | 38 | require(declaration.body == null) 39 | 40 | val pokoFunction = when { 41 | declaration.isEquals() -> PokoFunction.Equals 42 | declaration.isHashCode() -> PokoFunction.HashCode 43 | declaration.isToString() -> PokoFunction.ToString 44 | else -> return 45 | } 46 | 47 | val pokoClass = declaration.parentAsClass 48 | val pokoProperties = pokoClass.pokoProperties(pokoAnnotation).also { 49 | if (it.isEmpty()) { 50 | messageCollector.log("No primary constructor properties") 51 | messageCollector.reportErrorOnClass( 52 | irClass = pokoClass, 53 | message = "Poko class primary constructor must have at least one not-skipped property", 54 | ) 55 | } 56 | } 57 | 58 | declaration.body = DeclarationIrBuilder( 59 | generatorContext = context, 60 | symbol = declaration.symbol, 61 | ).irBlockBody { 62 | when (pokoFunction) { 63 | PokoFunction.Equals -> generateEqualsMethodBody( 64 | pokoAnnotation = pokoAnnotation, 65 | context = this@PokoFunctionBodyFiller.context, 66 | irClass = pokoClass, 67 | functionDeclaration = declaration, 68 | classProperties = pokoProperties, 69 | messageCollector = messageCollector, 70 | ) 71 | 72 | PokoFunction.HashCode -> generateHashCodeMethodBody( 73 | pokoAnnotation = pokoAnnotation, 74 | context = this@PokoFunctionBodyFiller.context, 75 | functionDeclaration = declaration, 76 | classProperties = pokoProperties, 77 | messageCollector = messageCollector, 78 | ) 79 | 80 | PokoFunction.ToString -> generateToStringMethodBody( 81 | pokoAnnotation = pokoAnnotation, 82 | context = this@PokoFunctionBodyFiller.context, 83 | irClass = pokoClass, 84 | functionDeclaration = declaration, 85 | classProperties = pokoProperties, 86 | messageCollector = messageCollector, 87 | ) 88 | } 89 | } 90 | } 91 | 92 | private fun interestedIn( 93 | key: GeneratedDeclarationKey?, 94 | ): Boolean { 95 | return key == PokoKey 96 | } 97 | 98 | override fun visitElement(element: IrElement) { 99 | when (element) { 100 | is IrDeclaration, is IrFile, is IrModuleFragment -> element.acceptChildrenVoid(this) 101 | else -> Unit 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/functionGeneration.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import dev.drewhamilton.poko.PokoAnnotationNames 4 | import org.jetbrains.kotlin.builtins.PrimitiveType 5 | import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder 6 | import org.jetbrains.kotlin.ir.builders.IrGeneratorContextInterface 7 | import org.jetbrains.kotlin.ir.declarations.IrClass 8 | import org.jetbrains.kotlin.ir.declarations.IrFunction 9 | import org.jetbrains.kotlin.ir.declarations.IrProperty 10 | import org.jetbrains.kotlin.ir.declarations.IrTypeParameter 11 | import org.jetbrains.kotlin.ir.declarations.IrValueParameter 12 | import org.jetbrains.kotlin.ir.expressions.IrGetValue 13 | import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl 14 | import org.jetbrains.kotlin.ir.symbols.IrClassSymbol 15 | import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol 16 | import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol 17 | import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI 18 | import org.jetbrains.kotlin.ir.types.IrSimpleType 19 | import org.jetbrains.kotlin.ir.types.classOrNull 20 | import org.jetbrains.kotlin.ir.types.classifierOrNull 21 | import org.jetbrains.kotlin.ir.types.createType 22 | import org.jetbrains.kotlin.ir.types.impl.IrStarProjectionImpl 23 | import org.jetbrains.kotlin.ir.util.hasAnnotation 24 | import org.jetbrains.kotlin.ir.util.isAnnotationClass 25 | import org.jetbrains.kotlin.ir.util.isInterface 26 | import org.jetbrains.kotlin.ir.util.render 27 | import org.jetbrains.kotlin.ir.util.superTypes 28 | import org.jetbrains.kotlin.name.ClassId 29 | 30 | /** 31 | * The type of an [IrProperty]. 32 | */ 33 | internal val IrProperty.type 34 | get() = backingField?.type 35 | ?: getter?.returnType 36 | ?: error("Can't find type of ${render()}") 37 | 38 | /** 39 | * The receiver value (i.e. `this`) for a function with a dispatch (i.e. non-extension) receiver. 40 | * 41 | * In the context of Poko, only works properly after the overridden method has had its 42 | * `dispatchReceiverParameter` updated to the current parent class. 43 | */ 44 | internal fun IrBlockBodyBuilder.receiver( 45 | function: IrFunction, 46 | ): IrGetValue = IrGetValueImpl(function.dispatchReceiverParameter!!) 47 | 48 | /** 49 | * Gets the value of the given [parameter]. 50 | */ 51 | internal fun IrBlockBodyBuilder.IrGetValueImpl( 52 | parameter: IrValueParameter, 53 | ): IrGetValueImpl { 54 | return IrGetValueImpl( 55 | startOffset = startOffset, 56 | endOffset = endOffset, 57 | type = parameter.type, 58 | symbol = parameter.symbol, 59 | ) 60 | } 61 | 62 | internal fun IrProperty.hasReadArrayContentAnnotation( 63 | pokoAnnotation: ClassId, 64 | ): Boolean = hasAnnotation( 65 | classId = pokoAnnotation.createNestedClassId(PokoAnnotationNames.ReadArrayContent), 66 | ) 67 | 68 | /** 69 | * Returns true if the classifier represents a type that may be an array at runtime (e.g. [Any] or 70 | * a generic type). 71 | */ 72 | internal fun IrClassifierSymbol?.mayBeRuntimeArray( 73 | context: IrGeneratorContextInterface, 74 | ): Boolean { 75 | return this == context.irBuiltIns.anyClass || 76 | (this is IrTypeParameterSymbol && hasArrayOrPrimitiveArrayUpperBound(context)) 77 | } 78 | 79 | private fun IrTypeParameterSymbol.hasArrayOrPrimitiveArrayUpperBound( 80 | context: IrGeneratorContextInterface, 81 | ): Boolean { 82 | superTypes().forEach { superType -> 83 | val superTypeClassifier = superType.classifierOrNull 84 | // Note: A generic type cannot have an array as an upper bound, else that would also 85 | // be checked here. 86 | val foundUpperBoundMatch = superTypeClassifier == context.irBuiltIns.anyClass || 87 | (superTypeClassifier is IrTypeParameterSymbol && 88 | superTypeClassifier.hasArrayOrPrimitiveArrayUpperBound(context)) 89 | 90 | if (foundUpperBoundMatch) { 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | 97 | @OptIn(UnsafeDuringIrConstructionAPI::class) 98 | internal val IrTypeParameter.erasedUpperBound: IrClass 99 | get() { 100 | // Pick the (necessarily unique) non-interface upper bound if it exists 101 | for (type in superTypes) { 102 | val irClass = type.classOrNull?.owner ?: continue 103 | if (!irClass.isInterface && !irClass.isAnnotationClass) return irClass 104 | } 105 | 106 | // Otherwise, choose either the first IrClass supertype or recurse. 107 | // In the first case, all supertypes are interface types and the choice was arbitrary. 108 | // In the second case, there is only a single supertype. 109 | return when (val firstSuper = superTypes.first().classifierOrNull?.owner) { 110 | is IrClass -> firstSuper 111 | is IrTypeParameter -> @Suppress("RecursivePropertyAccessor") firstSuper.erasedUpperBound 112 | else -> error("unknown supertype kind $firstSuper") 113 | } 114 | } 115 | 116 | internal fun PrimitiveType.toPrimitiveArrayClassSymbol( 117 | context: IrGeneratorContextInterface, 118 | ): IrClassSymbol { 119 | return context.irBuiltIns.primitiveTypesToPrimitiveArrays.getValue(this) 120 | } 121 | 122 | internal fun IrClassSymbol.createArrayType( 123 | context: IrGeneratorContextInterface, 124 | ): IrSimpleType { 125 | val typeArguments = when { 126 | this == context.irBuiltIns.arrayClass -> listOf(IrStarProjectionImpl) 127 | this in context.irBuiltIns.primitiveArraysToPrimitiveTypes -> emptyList() 128 | else -> throw IllegalArgumentException("$this is not an array class symbol") 129 | } 130 | return createType(hasQuestionMark = false, arguments = typeArguments) 131 | } 132 | -------------------------------------------------------------------------------- /poko-tests/src/commonTest/kotlin/ComplexTest.kt: -------------------------------------------------------------------------------- 1 | import assertk.all 2 | import assertk.assertAll 3 | import assertk.assertThat 4 | import assertk.assertions.hashCodeFun 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isNotEqualTo 7 | import assertk.assertions.toStringFun 8 | import kotlin.test.Test 9 | import data.Complex as ComplexData 10 | import poko.Complex as ComplexPoko 11 | 12 | class ComplexTest { 13 | @Test fun two_equivalent_compiled_Complex_instances_match() { 14 | val arrayReferenceType = arrayOf("one string", "another string") 15 | val arrayPrimitiveType = intArrayOf(3, 4, 5) 16 | val a = ComplexPoko( 17 | referenceType = "Text", 18 | nullableReferenceType = null, 19 | boolean = true, 20 | nullableBoolean = null, 21 | int = 2, 22 | nullableInt = null, 23 | long = 12345L, 24 | float = 67f, 25 | double = 89.0, 26 | arrayReferenceType = arrayReferenceType, 27 | nullableArrayReferenceType = null, 28 | arrayPrimitiveType = arrayPrimitiveType, 29 | nullableArrayPrimitiveType = null, 30 | genericCollectionType = listOf(4, 6, 8).map { EvenInt(it) }, 31 | nullableGenericCollectionType = null, 32 | genericType = EvenInt(2), 33 | nullableGenericType = null, 34 | ) 35 | val b = ComplexPoko( 36 | referenceType = "Text", 37 | nullableReferenceType = null, 38 | boolean = true, 39 | nullableBoolean = null, 40 | int = 2, 41 | nullableInt = null, 42 | long = 12345L, 43 | float = 67f, 44 | double = 89.0, 45 | arrayReferenceType = arrayReferenceType, 46 | nullableArrayReferenceType = null, 47 | arrayPrimitiveType = arrayPrimitiveType, 48 | nullableArrayPrimitiveType = null, 49 | genericCollectionType = listOf(4, 6, 8).map { EvenInt(it) }, 50 | nullableGenericCollectionType = null, 51 | genericType = EvenInt(2), 52 | nullableGenericType = null, 53 | ) 54 | assertAll { 55 | assertThat(a).isEqualTo(b) 56 | assertThat(b).isEqualTo(a) 57 | 58 | assertThat(a).hashCodeFun().isEqualTo(b.hashCode()) 59 | assertThat(a).toStringFun().isEqualTo(b.toString()) 60 | } 61 | } 62 | 63 | @Test fun two_inequivalent_compiled_Complex_instances_are_not_equals() { 64 | val arrayReferenceType = arrayOf("one string", "another string") 65 | val arrayPrimitiveType = intArrayOf(3, 4, 5) 66 | val a = ComplexPoko( 67 | referenceType = "Text", 68 | nullableReferenceType = null, 69 | boolean = true, 70 | nullableBoolean = null, 71 | int = 2, 72 | nullableInt = null, 73 | long = 12345L, 74 | float = 67f, 75 | double = 89.0, 76 | arrayReferenceType = arrayReferenceType, 77 | nullableArrayReferenceType = null, 78 | arrayPrimitiveType = arrayPrimitiveType, 79 | nullableArrayPrimitiveType = null, 80 | genericCollectionType = listOf(4, 6, 8).map { EvenInt(it) }, 81 | nullableGenericCollectionType = null, 82 | genericType = EvenInt(2), 83 | nullableGenericType = null, 84 | ) 85 | val b = ComplexPoko( 86 | referenceType = "Text", 87 | nullableReferenceType = "non-null", 88 | boolean = true, 89 | nullableBoolean = null, 90 | int = 2, 91 | nullableInt = null, 92 | long = 12345L, 93 | float = 67f, 94 | double = 89.0, 95 | arrayReferenceType = arrayReferenceType, 96 | nullableArrayReferenceType = null, 97 | arrayPrimitiveType = arrayPrimitiveType, 98 | nullableArrayPrimitiveType = null, 99 | genericCollectionType = listOf(4, 6, 8).map { EvenInt(it) }, 100 | nullableGenericCollectionType = null, 101 | genericType = EvenInt(2), 102 | nullableGenericType = null, 103 | ) 104 | assertThat(a).isNotEqualTo(b) 105 | assertThat(b).isNotEqualTo(a) 106 | } 107 | 108 | @Test fun compiled_Complex_class_instance_has_expected_hashCode_and_toString() { 109 | val arrayReferenceType = arrayOf("one string", "another string") 110 | val arrayPrimitiveType = intArrayOf(3, 4, 5) 111 | val poko = ComplexPoko( 112 | referenceType = "Text", 113 | nullableReferenceType = null, 114 | boolean = true, 115 | nullableBoolean = null, 116 | int = 2, 117 | nullableInt = null, 118 | long = 12345L, 119 | float = 67f, 120 | double = 89.0, 121 | arrayReferenceType = arrayReferenceType, 122 | nullableArrayReferenceType = null, 123 | arrayPrimitiveType = arrayPrimitiveType, 124 | nullableArrayPrimitiveType = null, 125 | genericCollectionType = listOf(4, 6, 8).map { EvenInt(it) }, 126 | nullableGenericCollectionType = null, 127 | genericType = EvenInt(2), 128 | nullableGenericType = null, 129 | ) 130 | val data = ComplexData( 131 | referenceType = "Text", 132 | nullableReferenceType = null, 133 | boolean = true, 134 | nullableBoolean = null, 135 | int = 2, 136 | nullableInt = null, 137 | long = 12345L, 138 | float = 67f, 139 | double = 89.0, 140 | arrayReferenceType = arrayReferenceType, 141 | nullableArrayReferenceType = null, 142 | arrayPrimitiveType = arrayPrimitiveType, 143 | nullableArrayPrimitiveType = null, 144 | genericCollectionType = listOf(4, 6, 8).map { EvenInt(it) }, 145 | nullableGenericCollectionType = null, 146 | genericType = EvenInt(2), 147 | nullableGenericType = null, 148 | ) 149 | assertThat(poko).all { 150 | hashCodeFun().isEqualTo(data.hashCode()) 151 | toStringFun().isEqualTo(data.toString()) 152 | } 153 | } 154 | 155 | data class EvenInt(private val value: Int) : Number() { 156 | init { check(value % 2 == 0) } 157 | override fun toByte() = value.toByte() 158 | override fun toDouble() = value.toDouble() 159 | override fun toFloat() = value.toFloat() 160 | override fun toInt() = value 161 | override fun toLong() = value.toLong() 162 | override fun toShort() = value.toShort() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poko 2 | [![CI status badge](https://github.com/drewhamilton/Poko/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/drewhamilton/Poko/actions/workflows/ci.yml?query=branch%3Amain) 3 | 4 | Poko is a Kotlin compiler plugin that makes writing and maintaining data model classes for public 5 | APIs easy. Like with normal Kotlin data classes, all you have to do is provide properties in your 6 | class's primary constructor. With the `@Poko` annotation, this compiler plugin automatically 7 | generates `toString`, `equals`, and `hashCode` functions. Poko is compatible with all Kotlin 8 | Multiplatform targets. 9 | 10 | ## Use 11 | Mark your class as a `@Poko class` instead of a `data class`: 12 | ```kotlin 13 | @Poko class MyData( 14 | val int: Int, 15 | val requiredString: String, 16 | val optionalString: String?, 17 | ) 18 | ``` 19 | 20 | And reap the benefits of a readable `toString` and working `equals` and `hashCode`. Unlike normal 21 | data classes, no `copy` or `componentN` functions are generated. 22 | 23 | Like normal data classes, Poko classes must have at least one property in their primary constructor. 24 | Non-property parameters in the primary constructor are ignored, as are non-constructor properties. 25 | Any of the three generated functions can be overridden manually, in which case Poko will not 26 | generate that function but will still generate the non-overridden functions. Using array properties 27 | is not recommended, and if they are used, it is recommended to override `equals` and `hashCode` 28 | manually. 29 | 30 | ### Annotation 31 | By default, the `dev.drewhamilton.poko.Poko` annotation is used to mark classes for Poko generation. 32 | If you prefer, you can create a different annotation and supply it to the Gradle plugin. 33 | 34 | ```groovy 35 | apply plugin: "dev.drewhamilton.poko" 36 | poko { 37 | pokoAnnotation.set "com/example/MyData" 38 | } 39 | ``` 40 | 41 | Nested annotations mentioned below can optionally be added with the same name to the base annotation 42 | and used for their respective features. For example, `@MyData.ReadArrayContent` will cause the 43 | annotated property's contents to be used in the Poko-generated functions. 44 | 45 | ### Independent function generation 46 | Use `@Poko.EqualsAndHashCode` instead of the base `@Poko` annotation to generate only the `equals` 47 | and `hashCode` functions for the annotated class. Use `@Poko.ToString` to generate only the 48 | `toString` function. 49 | 50 | ### Arrays 51 | By default, Poko does nothing to inspect the contents of array properties. [This aligns with the 52 | behavior of data classes](https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/#Appendix.Comparingarrays). 53 | 54 | Poko consumers can change this behavior on a per-property basis with the `@Poko.ReadArrayContent` 55 | annotation. On properties of a typed array type, this annotation will generate a `contentDeepEquals` 56 | check. On properties of a primitive array type, this annotation will generate a `contentEquals` 57 | check. And on properties of type `Any` or of a generic type, this annotation will generate a `when` 58 | statement that disambiguates the many array types at runtime and uses the appropriate 59 | `contentDeepEquals` or `contentEquals` check. In all cases, the corresponding content-based 60 | `hashCode` and `toString` are generated for the property as well. 61 | 62 | Using arrays as properties in data types is not generally recommended: arrays are mutable, and 63 | mutating data can affect the results of `equals` and `hashCode` over time, which is generally 64 | unexpected. For this reason, `@Poko.ReadArrayContent` should only be used in very 65 | performance-sensitive APIs. 66 | 67 | ### Skipping properties 68 | 69 | It is sometimes useful to omit some properties from consideration when generating the Poko 70 | functions. This can be done with the experimental `@Poko.Skip` annotation. Properties with this 71 | annotation will be excluded from all three generated functions. For example: 72 | 73 | ```kotlin 74 | @Poko class Data( 75 | val id: String, 76 | @Poko.Skip val callback: () -> Unit, 77 | ) : CircuitUiState 78 | 79 | Data("a") { println("a") } == Data("a") { println("not a") } // yields `true` 80 | ``` 81 | 82 | ### Download 83 | 84 | [![Maven Central](https://img.shields.io/maven-metadata/v.svg?label=maven%20central&metadataUrl=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Fdev%2Fdrewhamilton%2Fpoko%2Fpoko-compiler-plugin%2Fmaven-metadata.xml&color=blue)](https://central.sonatype.com/namespace/dev.drewhamilton.poko) 85 | 86 | Poko is available on Maven Central. It is experimental, and the API may undergo breaking changes 87 | before version 1.0.0. Kotlin compiler plugins in general are experimental and new versions of Kotlin 88 | might break something in this compiler plugin. 89 | 90 | Since the Kotlin compiler has frequent breaking changes, different versions of Kotlin are 91 | exclusively compatible with specific versions of Poko. 92 | 93 | | Kotlin version | Poko version | 94 | |-----------------|--------------| 95 | | 2.3.0 | 0.21.0 | 96 | | 2.2.20 – 2.2.21 | 0.20.2 | 97 | | 2.2.0 – 2.2.10 | 0.19.3 | 98 | | 2.1.0 – 2.1.21 | 0.18.7 | 99 | | 2.0.0 – 2.0.21 | 0.17.2 | 100 | | 1.9.0 – 1.9.24 | 0.15.3 | 101 | | 1.8.20 – 1.8.22 | 0.13.1 | 102 | | 1.8.0 – 1.8.10 | 0.12.0 | 103 | | 1.7.0 – 1.7.21 | 0.11.0 | 104 | | 1.6.20 – 1.6.21 | 0.10.0 | 105 | | 1.6.0 – 1.6.10 | 0.9.0 | 106 | | 1.5.0 – 1.5.31 | 0.8.1 | 107 | | 1.4.30 – 1.4.32 | 0.7.4 | 108 | | 1.4.20 – 1.4.21 | 0.5.0* | 109 | | 1.4.0 – 1.4.10 | 0.3.1* | 110 | | 1.3.72 | 0.2.4* | 111 | 112 | \*Versions prior to 0.7.0 use plugin name `dev.drewhamilton.extracare`. 113 | 114 | Snapshots of the development version are available in [Sonatype's Snapshots 115 | repository](https://central.sonatype.com/repository/maven-snapshots/). 116 | 117 | Releases are signed with [this key](https://keyserver.ubuntu.com/pks/lookup?search=09939C73246B4BA7444CAA453D002DBC5EA9615F&fingerprint=on&op=index). 118 | ``` 119 | pub rsa4096 2020-02-02 120 | 09939C73246B4BA7444CAA453D002DBC5EA9615F 121 | uid Drew Hamilton 122 | sig 3D002DBC5EA9615F 2020-02-02 123 | ``` 124 | 125 | To use Poko, apply the Gradle plugin in your project: 126 | ```kotlin 127 | // Root project: 128 | plugins { 129 | id("dev.drewhamilton.poko") apply false 130 | } 131 | 132 | // Per module: 133 | plugins { 134 | id("dev.drewhamilton.poko") 135 | } 136 | ``` 137 | 138 | ## License 139 | ``` 140 | Copyright 2020 Drew Hamilton 141 | 142 | Licensed under the Apache License, Version 2.0 (the "License"); 143 | you may not use this file except in compliance with the License. 144 | You may obtain a copy of the License at 145 | 146 | http://www.apache.org/licenses/LICENSE-2.0 147 | 148 | Unless required by applicable law or agreed to in writing, software 149 | distributed under the License is distributed on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 151 | See the License for the specific language governing permissions and 152 | limitations under the License. 153 | ``` 154 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/toStringGeneration.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 4 | import org.jetbrains.kotlin.builtins.PrimitiveType 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder 7 | import org.jetbrains.kotlin.ir.builders.irBranch 8 | import org.jetbrains.kotlin.ir.builders.irCall 9 | import org.jetbrains.kotlin.ir.builders.irConcat 10 | import org.jetbrains.kotlin.ir.builders.irElseBranch 11 | import org.jetbrains.kotlin.ir.builders.irGetField 12 | import org.jetbrains.kotlin.ir.builders.irImplicitCast 13 | import org.jetbrains.kotlin.ir.builders.irIs 14 | import org.jetbrains.kotlin.ir.builders.irReturn 15 | import org.jetbrains.kotlin.ir.builders.irString 16 | import org.jetbrains.kotlin.ir.builders.irWhen 17 | import org.jetbrains.kotlin.ir.declarations.IrClass 18 | import org.jetbrains.kotlin.ir.declarations.IrFunction 19 | import org.jetbrains.kotlin.ir.declarations.IrParameterKind 20 | import org.jetbrains.kotlin.ir.declarations.IrProperty 21 | import org.jetbrains.kotlin.ir.expressions.IrBranch 22 | import org.jetbrains.kotlin.ir.expressions.IrExpression 23 | import org.jetbrains.kotlin.ir.expressions.addArgument 24 | import org.jetbrains.kotlin.ir.symbols.IrClassSymbol 25 | import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol 26 | import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol 27 | import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI 28 | import org.jetbrains.kotlin.ir.types.classifierOrFail 29 | import org.jetbrains.kotlin.ir.types.classifierOrNull 30 | import org.jetbrains.kotlin.ir.util.isArrayOrPrimitiveArray 31 | import org.jetbrains.kotlin.ir.util.isNullable 32 | import org.jetbrains.kotlin.name.CallableId 33 | import org.jetbrains.kotlin.name.ClassId 34 | import org.jetbrains.kotlin.name.FqName 35 | import org.jetbrains.kotlin.name.Name 36 | 37 | /** 38 | * Generate the body of the toString method. Adapted from 39 | * [org.jetbrains.kotlin.ir.util.DataClassMembersGenerator.MemberFunctionBuilder.generateToStringMethodBody]. 40 | */ 41 | @UnsafeDuringIrConstructionAPI 42 | internal fun IrBlockBodyBuilder.generateToStringMethodBody( 43 | pokoAnnotation: ClassId, 44 | context: IrPluginContext, 45 | irClass: IrClass, 46 | functionDeclaration: IrFunction, 47 | classProperties: List, 48 | messageCollector: MessageCollector, 49 | ) { 50 | val irConcat = irConcat() 51 | irConcat.addArgument(irString(irClass.name.asString() + "(")) 52 | 53 | var first = true 54 | for (property in classProperties) { 55 | if (!first) irConcat.addArgument(irString(", ")) 56 | 57 | irConcat.addArgument(irString(property.name.asString() + "=")) 58 | 59 | val propertyValue = irGetField(receiver(functionDeclaration), property.backingField!!) 60 | 61 | val classifier = property.type.classifierOrNull 62 | val hasReadArrayContentAnnotation = property.hasReadArrayContentAnnotation(pokoAnnotation) 63 | val propertyStringValue = when { 64 | hasReadArrayContentAnnotation && classifier.mayBeRuntimeArray(context) -> { 65 | val field = property.backingField!! 66 | val instance = irGetField(receiver(functionDeclaration), field) 67 | irRuntimeArrayContentDeepToString(context, instance) 68 | } 69 | 70 | hasReadArrayContentAnnotation -> { 71 | val toStringFunctionSymbol = maybeFindArrayDeepToStringFunction( 72 | context = context, 73 | property = property, 74 | messageCollector = messageCollector 75 | ) ?: context.irBuiltIns.dataClassArrayMemberToStringSymbol 76 | irCallToStringFunction( 77 | toStringFunctionSymbol = toStringFunctionSymbol, 78 | value = propertyValue, 79 | ) 80 | } 81 | 82 | classifier.isArrayOrPrimitiveArray(context.irBuiltIns) -> { 83 | irCallToStringFunction( 84 | toStringFunctionSymbol = context.irBuiltIns.dataClassArrayMemberToStringSymbol, 85 | value = propertyValue, 86 | ) 87 | } 88 | 89 | else -> propertyValue 90 | } 91 | 92 | irConcat.addArgument(propertyStringValue) 93 | first = false 94 | } 95 | irConcat.addArgument(irString(")")) 96 | +irReturn(irConcat) 97 | } 98 | 99 | /** 100 | * Returns `contentDeepToString` function symbol if it is an appropriate option for [property], 101 | * else returns null. 102 | */ 103 | @UnsafeDuringIrConstructionAPI 104 | private fun maybeFindArrayDeepToStringFunction( 105 | context: IrPluginContext, 106 | property: IrProperty, 107 | messageCollector: MessageCollector, 108 | ): IrSimpleFunctionSymbol? { 109 | val propertyClassifier = property.type.classifierOrFail 110 | 111 | val isArray = propertyClassifier.isArrayOrPrimitiveArray(context.irBuiltIns) 112 | if (!isArray) { 113 | messageCollector.reportErrorOnProperty( 114 | property = property, 115 | message = "@ReadArrayContent is only supported on properties with array type or `Any` type", 116 | ) 117 | return null 118 | } 119 | 120 | return findContentDeepToStringFunctionSymbol(context, propertyClassifier) 121 | } 122 | 123 | /** 124 | * Generates a `when` branch that checks the runtime type of the [value] instance and invokes 125 | * `contentDeepToString` or `contentToString` for typed arrays and primitive arrays, respectively. 126 | */ 127 | @UnsafeDuringIrConstructionAPI 128 | private fun IrBlockBodyBuilder.irRuntimeArrayContentDeepToString( 129 | context: IrPluginContext, 130 | value: IrExpression, 131 | ): IrExpression { 132 | return irWhen( 133 | type = context.irBuiltIns.stringType, 134 | branches = listOf( 135 | irArrayTypeCheckAndContentDeepToStringBranch( 136 | context = context, 137 | value = value, 138 | classSymbol = context.irBuiltIns.arrayClass, 139 | ), 140 | 141 | // Map each primitive type to a `when` branch covering its respective primitive array 142 | // type: 143 | *PrimitiveType.entries.map { primitiveType -> 144 | irArrayTypeCheckAndContentDeepToStringBranch( 145 | context = context, 146 | value = value, 147 | classSymbol = primitiveType.toPrimitiveArrayClassSymbol(context), 148 | ) 149 | }.toTypedArray(), 150 | 151 | irElseBranch( 152 | irCallToStringFunction( 153 | toStringFunctionSymbol = context.irBuiltIns.extensionToString, 154 | value = value, 155 | ), 156 | ), 157 | ), 158 | ) 159 | } 160 | 161 | /** 162 | * Generates a runtime `when` branch computing the content deep toString of [value]. The branch is 163 | * only executed if [value] is an instance of [classSymbol]. 164 | */ 165 | @UnsafeDuringIrConstructionAPI 166 | private fun IrBlockBodyBuilder.irArrayTypeCheckAndContentDeepToStringBranch( 167 | context: IrPluginContext, 168 | value: IrExpression, 169 | classSymbol: IrClassSymbol, 170 | ): IrBranch { 171 | val type = classSymbol.createArrayType(context) 172 | return irBranch( 173 | condition = irIs(value, type), 174 | result = irCallToStringFunction( 175 | toStringFunctionSymbol = findContentDeepToStringFunctionSymbol(context, classSymbol), 176 | value = irImplicitCast(value, type), 177 | ), 178 | ) 179 | } 180 | 181 | /** 182 | * Finds `contentDeepToString` function if [propertyClassifier] is a typed array, or 183 | * `contentToString` function if it is a primitive array. 184 | */ 185 | @UnsafeDuringIrConstructionAPI 186 | private fun findContentDeepToStringFunctionSymbol( 187 | context: IrPluginContext, 188 | propertyClassifier: IrClassifierSymbol, 189 | ): IrSimpleFunctionSymbol { 190 | val callableName = if (propertyClassifier == context.irBuiltIns.arrayClass) { 191 | "contentDeepToString" 192 | } else { 193 | "contentToString" 194 | } 195 | return context.referenceFunctions( 196 | callableId = CallableId( 197 | packageName = FqName("kotlin.collections"), 198 | callableName = Name.identifier(callableName), 199 | ), 200 | ).single { functionSymbol -> 201 | // Find the single function with the relevant array type and disambiguate against the 202 | // older non-nullable receiver overload: 203 | val extensionReceiverParameter = functionSymbol.owner.parameters 204 | .singleOrNull { it.kind == IrParameterKind.ExtensionReceiver } 205 | return@single extensionReceiverParameter?.type?.let { 206 | it.classifierOrNull == propertyClassifier && it.isNullable() 207 | } ?: false 208 | } 209 | } 210 | 211 | @UnsafeDuringIrConstructionAPI 212 | private fun IrBlockBodyBuilder.irCallToStringFunction( 213 | toStringFunctionSymbol: IrSimpleFunctionSymbol, 214 | value: IrExpression, 215 | ): IrExpression { 216 | return irCall( 217 | callee = toStringFunctionSymbol, 218 | type = context.irBuiltIns.stringType, 219 | ).apply { 220 | toStringFunctionSymbol.owner.parameters.forEach { 221 | arguments.set( 222 | parameter = it, 223 | value = when (it.kind) { 224 | IrParameterKind.ExtensionReceiver -> value 225 | IrParameterKind.Regular -> value 226 | else -> throw IllegalArgumentException("toString unknown param type") 227 | } 228 | ) 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/ir/equalsGeneration.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.ir 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 4 | import org.jetbrains.kotlin.backend.common.lower.irNot 5 | import org.jetbrains.kotlin.builtins.PrimitiveType 6 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 7 | import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder 8 | import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope 9 | import org.jetbrains.kotlin.ir.builders.irBranch 10 | import org.jetbrains.kotlin.ir.builders.irCall 11 | import org.jetbrains.kotlin.ir.builders.irElseBranch 12 | import org.jetbrains.kotlin.ir.builders.irEqeqeq 13 | import org.jetbrains.kotlin.ir.builders.irEquals 14 | import org.jetbrains.kotlin.ir.builders.irFalse 15 | import org.jetbrains.kotlin.ir.builders.irGet 16 | import org.jetbrains.kotlin.ir.builders.irGetField 17 | import org.jetbrains.kotlin.ir.builders.irIfThenElse 18 | import org.jetbrains.kotlin.ir.builders.irIfThenReturnFalse 19 | import org.jetbrains.kotlin.ir.builders.irIfThenReturnTrue 20 | import org.jetbrains.kotlin.ir.builders.irImplicitCast 21 | import org.jetbrains.kotlin.ir.builders.irIs 22 | import org.jetbrains.kotlin.ir.builders.irNotEquals 23 | import org.jetbrains.kotlin.ir.builders.irNotIs 24 | import org.jetbrains.kotlin.ir.builders.irReturnTrue 25 | import org.jetbrains.kotlin.ir.builders.irTemporary 26 | import org.jetbrains.kotlin.ir.builders.irWhen 27 | import org.jetbrains.kotlin.ir.declarations.IrClass 28 | import org.jetbrains.kotlin.ir.declarations.IrFunction 29 | import org.jetbrains.kotlin.ir.declarations.IrParameterKind 30 | import org.jetbrains.kotlin.ir.declarations.IrProperty 31 | import org.jetbrains.kotlin.ir.expressions.IrBranch 32 | import org.jetbrains.kotlin.ir.expressions.IrExpression 33 | import org.jetbrains.kotlin.ir.symbols.IrClassSymbol 34 | import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol 35 | import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol 36 | import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI 37 | import org.jetbrains.kotlin.ir.types.classifierOrFail 38 | import org.jetbrains.kotlin.ir.types.classifierOrNull 39 | import org.jetbrains.kotlin.ir.util.defaultType 40 | import org.jetbrains.kotlin.ir.util.isArrayOrPrimitiveArray 41 | import org.jetbrains.kotlin.ir.util.isNullable 42 | import org.jetbrains.kotlin.name.CallableId 43 | import org.jetbrains.kotlin.name.ClassId 44 | import org.jetbrains.kotlin.name.FqName 45 | import org.jetbrains.kotlin.name.Name 46 | 47 | /** 48 | * Generate the body of the equals method. Adapted from 49 | * [org.jetbrains.kotlin.ir.util.DataClassMembersGenerator.MemberFunctionBuilder.generateEqualsMethodBody]. 50 | */ 51 | @UnsafeDuringIrConstructionAPI 52 | internal fun IrBlockBodyBuilder.generateEqualsMethodBody( 53 | pokoAnnotation: ClassId, 54 | context: IrPluginContext, 55 | irClass: IrClass, 56 | functionDeclaration: IrFunction, 57 | classProperties: List, 58 | messageCollector: MessageCollector, 59 | ) { 60 | val irType = irClass.defaultType 61 | fun irOther(): IrExpression = IrGetValueImpl( 62 | parameter = functionDeclaration.parameters.single { it.kind == IrParameterKind.Regular }, 63 | ) 64 | 65 | +irIfThenReturnTrue(irEqeqeq(receiver(functionDeclaration), irOther())) 66 | +irIfThenReturnFalse(irNotIs(irOther(), irType)) 67 | 68 | val otherWithCast = irTemporary(irImplicitCast(irOther(), irType), "other_with_cast") 69 | for (property in classProperties) { 70 | val field = property.backingField!! 71 | val arg1 = irGetField(receiver(functionDeclaration), field) 72 | val arg2 = irGetField(irGet(irType, otherWithCast.symbol), field) 73 | val irNotEquals = when { 74 | property.hasReadArrayContentAnnotation(pokoAnnotation) -> { 75 | irNot( 76 | irArrayContentDeepEquals( 77 | context = context, 78 | receiver = arg1, 79 | argument = arg2, 80 | property = property, 81 | messageCollector = messageCollector, 82 | ), 83 | ) 84 | } 85 | 86 | else -> { 87 | irNotEquals(arg1, arg2) 88 | } 89 | } 90 | +irIfThenReturnFalse(irNotEquals) 91 | } 92 | +irReturnTrue() 93 | } 94 | 95 | /** 96 | * Generates IR code that checks the equality of [receiver] and [argument] by content. If [property] 97 | * type is not an array type, but may be an array at runtime, generates a runtime type check. 98 | */ 99 | @UnsafeDuringIrConstructionAPI 100 | private fun IrBuilderWithScope.irArrayContentDeepEquals( 101 | context: IrPluginContext, 102 | receiver: IrExpression, 103 | argument: IrExpression, 104 | property: IrProperty, 105 | messageCollector: MessageCollector, 106 | ): IrExpression { 107 | val propertyType = property.type 108 | val propertyClassifier = propertyType.classifierOrFail 109 | 110 | val isArray = propertyClassifier.isArrayOrPrimitiveArray(context.irBuiltIns) 111 | if (!isArray) { 112 | val mayBeRuntimeArray = propertyClassifier.mayBeRuntimeArray(context) 113 | return if (mayBeRuntimeArray) { 114 | irRuntimeArrayContentDeepEquals(context, receiver, argument) 115 | } else { 116 | messageCollector.reportErrorOnProperty( 117 | property = property, 118 | message = "@ReadArrayContent is only supported on properties with array type or `Any` type", 119 | ) 120 | irEquals(receiver, argument) 121 | } 122 | } 123 | 124 | return irCallContentDeepEquals( 125 | context = context, 126 | classifier = propertyClassifier, 127 | receiver = receiver, 128 | argument = argument, 129 | ) 130 | } 131 | 132 | /** 133 | * Generates IR code that checks the type of [receiver] at runtime, and performs an array content 134 | * equality check against [argument] if the type is an array type. 135 | */ 136 | @UnsafeDuringIrConstructionAPI 137 | private fun IrBuilderWithScope.irRuntimeArrayContentDeepEquals( 138 | context: IrPluginContext, 139 | receiver: IrExpression, 140 | argument: IrExpression, 141 | ): IrExpression { 142 | return irWhen( 143 | type = context.irBuiltIns.booleanType, 144 | branches = listOf( 145 | irArrayTypeCheckAndContentDeepEqualsBranch( 146 | context = context, 147 | receiver = receiver, 148 | argument = argument, 149 | classSymbol = context.irBuiltIns.arrayClass, 150 | ), 151 | 152 | // Map each primitive type to a `when` branch covering its respective primitive array 153 | // type: 154 | *PrimitiveType.entries.map { primitiveType -> 155 | irArrayTypeCheckAndContentDeepEqualsBranch( 156 | context = context, 157 | receiver = receiver, 158 | argument = argument, 159 | classSymbol = primitiveType.toPrimitiveArrayClassSymbol(context), 160 | ) 161 | }.toTypedArray(), 162 | 163 | irElseBranch( 164 | irEquals(receiver, argument), 165 | ), 166 | ), 167 | ) 168 | } 169 | 170 | /** 171 | * Generates a runtime `when` branch checking for content deep equality of [receiver] and 172 | * [argument]. The branch is only executed if [receiver] is an instance of [classSymbol]. 173 | */ 174 | @UnsafeDuringIrConstructionAPI 175 | private fun IrBuilderWithScope.irArrayTypeCheckAndContentDeepEqualsBranch( 176 | context: IrPluginContext, 177 | receiver: IrExpression, 178 | argument: IrExpression, 179 | classSymbol: IrClassSymbol, 180 | ): IrBranch { 181 | val type = classSymbol.createArrayType(context) 182 | return irBranch( 183 | condition = irIs(receiver, type), 184 | result = irIfThenElse( 185 | type = context.irBuiltIns.booleanType, 186 | condition = irIs(argument, type), 187 | thenPart = irCallContentDeepEquals( 188 | context = context, 189 | classifier = classSymbol, 190 | receiver = irImplicitCast(receiver, type), 191 | argument = irImplicitCast(argument, type), 192 | ), 193 | elsePart = irFalse(), 194 | ), 195 | ) 196 | } 197 | 198 | @UnsafeDuringIrConstructionAPI 199 | private fun IrBuilderWithScope.irCallContentDeepEquals( 200 | context: IrPluginContext, 201 | classifier: IrClassifierSymbol, 202 | receiver: IrExpression, 203 | argument: IrExpression, 204 | ): IrExpression { 205 | val contentDeepEqualsFunctionSymbol = findContentDeepEqualsFunctionSymbol(context, classifier) 206 | return irCall( 207 | callee = contentDeepEqualsFunctionSymbol, 208 | type = context.irBuiltIns.booleanType, 209 | typeArgumentsCount = 1, 210 | ).apply { 211 | contentDeepEqualsFunctionSymbol.owner.parameters.forEach { 212 | arguments.set( 213 | parameter = it, 214 | value = when (it.kind) { 215 | IrParameterKind.ExtensionReceiver -> receiver 216 | IrParameterKind.Regular -> argument 217 | else -> throw IllegalArgumentException("contentDeepEquals unknown param type") 218 | } 219 | ) 220 | } 221 | } 222 | } 223 | 224 | /** 225 | * Finds `contentDeepEquals` function if [classifier] represents a typed array, or `contentEquals` 226 | * function if it represents a primitive array. 227 | */ 228 | @UnsafeDuringIrConstructionAPI 229 | private fun findContentDeepEqualsFunctionSymbol( 230 | context: IrPluginContext, 231 | classifier: IrClassifierSymbol, 232 | ): IrSimpleFunctionSymbol { 233 | val callableName = if (classifier == context.irBuiltIns.arrayClass) { 234 | "contentDeepEquals" 235 | } else { 236 | "contentEquals" 237 | } 238 | return context.referenceFunctions( 239 | callableId = CallableId( 240 | packageName = FqName("kotlin.collections"), 241 | callableName = Name.identifier(callableName), 242 | ), 243 | ).single { functionSymbol -> 244 | // Find the single function with the relevant array type and disambiguate against the 245 | // older non-nullable receiver overload: 246 | val extensionReceiverParameter = functionSymbol.owner.parameters 247 | .singleOrNull { it.kind == IrParameterKind.ExtensionReceiver } 248 | return@single extensionReceiverParameter?.type?.let { 249 | it.classifierOrNull == classifier && it.isNullable() 250 | } ?: false 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/fir/PokoFirDeclarationGenerationExtension.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.fir 2 | 3 | import dev.drewhamilton.poko.PokoFunction 4 | import dev.drewhamilton.poko.PokoFunction.Equals 5 | import dev.drewhamilton.poko.PokoFunction.HashCode 6 | import dev.drewhamilton.poko.PokoFunction.ToString 7 | import org.jetbrains.kotlin.descriptors.ClassKind 8 | import org.jetbrains.kotlin.descriptors.Modality 9 | import org.jetbrains.kotlin.descriptors.Visibilities 10 | import org.jetbrains.kotlin.fir.FirSession 11 | import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction 12 | import org.jetbrains.kotlin.fir.declarations.processAllDeclaredCallables 13 | import org.jetbrains.kotlin.fir.declarations.utils.isExtension 14 | import org.jetbrains.kotlin.fir.declarations.utils.isFinal 15 | import org.jetbrains.kotlin.fir.declarations.utils.visibility 16 | import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension 17 | import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar 18 | import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext 19 | import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate 20 | import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider 21 | import org.jetbrains.kotlin.fir.plugin.createMemberFunction 22 | import org.jetbrains.kotlin.fir.resolve.toClassSymbol 23 | import org.jetbrains.kotlin.fir.scopes.impl.FirClassDeclaredMemberScope 24 | import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol 25 | import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol 26 | import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol 27 | import org.jetbrains.kotlin.fir.types.ConeKotlinType 28 | import org.jetbrains.kotlin.name.CallableId 29 | import org.jetbrains.kotlin.name.Name 30 | import org.jetbrains.kotlin.utils.addToStdlib.runIf 31 | 32 | internal class PokoFirDeclarationGenerationExtension( 33 | session: FirSession, 34 | ) : FirDeclarationGenerationExtension(session) { 35 | //region Poko 36 | private val pokoAnnotation by lazy { 37 | session.pokoFirExtensionSessionComponent.pokoAnnotation 38 | } 39 | private val pokoAnnotationPredicate by lazy { 40 | LookupPredicate.create { 41 | annotated(pokoAnnotation.asSingleFqName()) 42 | } 43 | } 44 | 45 | /** 46 | * Pairs of . 47 | */ 48 | private val pokoClasses by lazy { 49 | session.predicateBasedProvider.getSymbolsByPredicate(pokoAnnotationPredicate) 50 | .filterIsInstance() 51 | } 52 | //endregion 53 | 54 | //region Poko.EqualsAndHashCode 55 | private val pokoEqualsAndHashCodeAnnotation by lazy { 56 | session.pokoFirExtensionSessionComponent.pokoEqualsAndHashCodeAnnotation 57 | } 58 | private val pokoEqualsAndHashCodeAnnotationPredicate by lazy { 59 | LookupPredicate.create { 60 | annotated(pokoEqualsAndHashCodeAnnotation.asSingleFqName()) 61 | } 62 | } 63 | 64 | /** 65 | * Pairs of . 66 | */ 67 | private val pokoEqualsAndHashCodeClasses by lazy { 68 | session.predicateBasedProvider 69 | .getSymbolsByPredicate(pokoEqualsAndHashCodeAnnotationPredicate) 70 | .filterIsInstance() 71 | } 72 | //endregion 73 | 74 | //region Poko.ToString 75 | private val pokoToStringAnnotation by lazy { 76 | session.pokoFirExtensionSessionComponent.pokoToStringAnnotation 77 | } 78 | private val pokoToStringAnnotationPredicate by lazy { 79 | LookupPredicate.create { 80 | annotated(pokoToStringAnnotation.asSingleFqName()) 81 | } 82 | } 83 | 84 | /** 85 | * Pairs of . 86 | */ 87 | private val pokoToStringClasses by lazy { 88 | session.predicateBasedProvider.getSymbolsByPredicate(pokoToStringAnnotationPredicate) 89 | .filterIsInstance() 90 | } 91 | //endregion 92 | 93 | override fun FirDeclarationPredicateRegistrar.registerPredicates() { 94 | register( 95 | pokoAnnotationPredicate, 96 | pokoEqualsAndHashCodeAnnotationPredicate, 97 | pokoToStringAnnotationPredicate, 98 | ) 99 | } 100 | 101 | override fun getCallableNamesForClass( 102 | classSymbol: FirClassSymbol<*>, 103 | context: MemberGenerationContext, 104 | ): Set = when (classSymbol) { 105 | in pokoClasses -> PokoFunction.entries.map { it.functionName }.toSet() 106 | in pokoEqualsAndHashCodeClasses -> setOf(Equals.functionName, HashCode.functionName) 107 | in pokoToStringClasses -> setOf(ToString.functionName) 108 | else -> emptySet() 109 | } 110 | 111 | override fun generateFunctions( 112 | callableId: CallableId, 113 | context: MemberGenerationContext? 114 | ): List { 115 | val owner = context?.owner ?: return emptyList() 116 | val scope = context.declaredScope ?: return emptyList() 117 | 118 | val callableName = callableId.callableName 119 | val function = with(scope) { 120 | when (callableName) { 121 | Equals.functionName -> runIf(owner.canGenerateFunction(Equals)) { 122 | createEqualsFunction(owner) 123 | } 124 | 125 | HashCode.functionName -> runIf(owner.canGenerateFunction(HashCode)) { 126 | createHashCodeFunction(owner) 127 | } 128 | 129 | ToString.functionName -> runIf(owner.canGenerateFunction(ToString)) { 130 | createToStringFunction(owner) 131 | } 132 | 133 | else -> null 134 | } 135 | } 136 | return function?.let { listOf(it.symbol) } ?: emptyList() 137 | } 138 | 139 | context(scope: FirClassDeclaredMemberScope) 140 | private fun FirClassSymbol<*>.canGenerateFunction(function: PokoFunction): Boolean { 141 | if (hasDeclaredFunction(function)) return false 142 | 143 | val superclassFunction = findNearestSuperclassFunction(function) 144 | 145 | return superclassFunction?.isOverridable ?: true 146 | } 147 | 148 | context(scope: FirClassDeclaredMemberScope) 149 | private fun hasDeclaredFunction(function: PokoFunction): Boolean { 150 | return declaredFunction(function) != null 151 | } 152 | 153 | /** 154 | * Finds the function symbol if the given [function] is declared in this scope. 155 | * 156 | * Used for the Poko class. 157 | */ 158 | context(scope: FirClassDeclaredMemberScope) 159 | private fun declaredFunction( 160 | function: PokoFunction, 161 | ): FirNamedFunctionSymbol? { 162 | val matchingFunctions = mutableListOf() 163 | scope.processFunctionsByName(function.functionName) { functionSymbol -> 164 | if ( 165 | !functionSymbol.isExtension && 166 | functionSymbol.valueParameterSymbols 167 | .map { it.resolvedReturnType } == function.valueParameterTypes() 168 | ) { 169 | matchingFunctions.add(functionSymbol) 170 | } 171 | } 172 | return matchingFunctions 173 | .apply { check(size < 2) { "Found multiple identical functions" } } 174 | .singleOrNull() 175 | } 176 | 177 | /** 178 | * Recursively finds the [function] in this class's nearest superclass with the same signature. 179 | * Ignores super-interfaces. 180 | */ 181 | private fun FirClassSymbol<*>.findNearestSuperclassFunction( 182 | function: PokoFunction, 183 | ): FirNamedFunctionSymbol? { 184 | val superclass = resolvedSuperTypes 185 | .mapNotNull { it.toClassSymbol(session) } 186 | .filter { it.classKind == ClassKind.CLASS } 187 | .apply { check(size < 2) { "Found multiple superclasses" } } 188 | .singleOrNull() 189 | ?: return null 190 | 191 | return superclass.declaredFunction(function) 192 | ?: superclass.findNearestSuperclassFunction(function) 193 | } 194 | 195 | /** 196 | * Finds the function symbol if the given [function] is declared in this class. 197 | * 198 | * Used for the Poko class's superclass(es). 199 | */ 200 | private fun FirClassSymbol<*>.declaredFunction( 201 | function: PokoFunction, 202 | ): FirNamedFunctionSymbol? { 203 | val matchingFunctions = mutableListOf() 204 | processAllDeclaredCallables(session) { callableSymbol -> 205 | if ( 206 | callableSymbol is FirNamedFunctionSymbol && 207 | !callableSymbol.isExtension && 208 | callableSymbol.name == function.functionName && 209 | callableSymbol.valueParameterSymbols 210 | .map { it.resolvedReturnType } == function.valueParameterTypes() 211 | ) { 212 | matchingFunctions.add(callableSymbol) 213 | } 214 | } 215 | return matchingFunctions 216 | .apply { check(size < 2) { "Found multiple identical functions" } } 217 | .singleOrNull() 218 | } 219 | 220 | private fun PokoFunction.valueParameterTypes(): List = when (this) { 221 | Equals -> listOf(session.builtinTypes.nullableAnyType.coneType) 222 | HashCode -> emptyList() 223 | ToString -> emptyList() 224 | } 225 | 226 | private val FirNamedFunctionSymbol.isOverridable: Boolean 227 | get() = visibility != Visibilities.Private && !isFinal 228 | 229 | private fun createEqualsFunction( 230 | owner: FirClassSymbol<*>, 231 | ): FirSimpleFunction = createMemberFunction( 232 | owner = owner, 233 | key = PokoKey, 234 | name = Equals.functionName, 235 | returnType = session.builtinTypes.booleanType.coneType, 236 | ) { 237 | modality = Modality.OPEN 238 | status { 239 | isOperator = true 240 | } 241 | valueParameter( 242 | name = Name.identifier("other"), 243 | type = session.builtinTypes.nullableAnyType.coneType, 244 | key = PokoKey, 245 | ) 246 | } 247 | 248 | private fun createHashCodeFunction( 249 | owner: FirClassSymbol<*>, 250 | ): FirSimpleFunction = createMemberFunction( 251 | owner = owner, 252 | key = PokoKey, 253 | name = HashCode.functionName, 254 | returnType = session.builtinTypes.intType.coneType, 255 | ) { 256 | modality = Modality.OPEN 257 | } 258 | 259 | private fun createToStringFunction( 260 | owner: FirClassSymbol<*>, 261 | ): FirSimpleFunction = createMemberFunction( 262 | owner = owner, 263 | key = PokoKey, 264 | name = ToString.functionName, 265 | returnType = session.builtinTypes.stringType.coneType, 266 | ) { 267 | modality = Modality.OPEN 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /poko-compiler-plugin/src/main/kotlin/dev/drewhamilton/poko/fir/PokoFirCheckersExtension.kt: -------------------------------------------------------------------------------- 1 | package dev.drewhamilton.poko.fir 2 | 3 | import org.jetbrains.kotlin.KtFakeSourceElementKind 4 | import org.jetbrains.kotlin.descriptors.ClassKind 5 | import org.jetbrains.kotlin.diagnostics.DiagnosticReporter 6 | import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap 7 | import org.jetbrains.kotlin.diagnostics.KtDiagnosticsContainer 8 | import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies 9 | import org.jetbrains.kotlin.diagnostics.error0 10 | import org.jetbrains.kotlin.diagnostics.rendering.BaseDiagnosticRendererFactory 11 | import org.jetbrains.kotlin.diagnostics.reportOn 12 | import org.jetbrains.kotlin.fir.FirSession 13 | import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind 14 | import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext 15 | import org.jetbrains.kotlin.fir.analysis.checkers.declaration.DeclarationCheckers 16 | import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirRegularClassChecker 17 | import org.jetbrains.kotlin.fir.analysis.checkers.hasModifier 18 | import org.jetbrains.kotlin.fir.analysis.extensions.FirAdditionalCheckersExtension 19 | import org.jetbrains.kotlin.fir.declarations.FirDeclaration 20 | import org.jetbrains.kotlin.fir.declarations.FirRegularClass 21 | import org.jetbrains.kotlin.fir.declarations.hasAnnotation 22 | import org.jetbrains.kotlin.fir.declarations.primaryConstructorIfAny 23 | import org.jetbrains.kotlin.fir.declarations.processAllDeclarations 24 | import org.jetbrains.kotlin.fir.declarations.utils.isData 25 | import org.jetbrains.kotlin.fir.declarations.utils.isInner 26 | import org.jetbrains.kotlin.fir.expressions.FirAnnotation 27 | import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol 28 | import org.jetbrains.kotlin.fir.types.ConeKotlinType 29 | import org.jetbrains.kotlin.fir.types.ConeTypeParameterType 30 | import org.jetbrains.kotlin.fir.types.classId 31 | import org.jetbrains.kotlin.fir.types.coneTypeOrNull 32 | import org.jetbrains.kotlin.fir.types.isArrayOrPrimitiveArray 33 | import org.jetbrains.kotlin.lexer.KtTokens 34 | import org.jetbrains.kotlin.name.ClassId 35 | import org.jetbrains.kotlin.psi.KtClass 36 | import org.jetbrains.kotlin.psi.KtProperty 37 | 38 | internal class PokoFirCheckersExtension( 39 | session: FirSession, 40 | ) : FirAdditionalCheckersExtension(session) { 41 | override val declarationCheckers: DeclarationCheckers = 42 | object : DeclarationCheckers() { 43 | override val regularClassCheckers: Set = 44 | setOf(PokoFirRegularClassChecker) 45 | } 46 | 47 | private object PokoFirRegularClassChecker : FirRegularClassChecker( 48 | mppKind = MppCheckerKind.Common, 49 | ) { 50 | context(context: CheckerContext, reporter: DiagnosticReporter) 51 | override fun check(declaration: FirRegularClass) { 52 | if (!declaration.hasAnyPokoClassAnnotation(context)) return 53 | 54 | val errorFactory = when { 55 | declaration.classKind != ClassKind.CLASS -> Diagnostics.PokoOnNonClass 56 | declaration.isData -> Diagnostics.PokoOnDataClass 57 | declaration.hasModifier(KtTokens.VALUE_KEYWORD) -> Diagnostics.PokoOnValueClass 58 | declaration.isInner -> Diagnostics.PokoOnInnerClass 59 | declaration.primaryConstructorIfAny(context.session) == null -> 60 | Diagnostics.PrimaryConstructorRequired 61 | else -> null 62 | } 63 | if (errorFactory != null) { 64 | reporter.reportOn( 65 | source = declaration.source, 66 | factory = errorFactory, 67 | ) 68 | } 69 | 70 | val sessionComponent = context.session.pokoFirExtensionSessionComponent 71 | val constructorProperties = mutableListOf() 72 | declaration.processAllDeclarations(context.session) { declarationSymbol -> 73 | if ( 74 | declarationSymbol is FirPropertySymbol && 75 | declarationSymbol.source?.kind is KtFakeSourceElementKind.PropertyFromParameter 76 | ) { 77 | constructorProperties.add(declarationSymbol) 78 | } 79 | } 80 | 81 | val skipAnnotation = sessionComponent.pokoSkipAnnotation 82 | val filteredConstructorProperties = constructorProperties 83 | .filter { 84 | !it.hasAnnotation(skipAnnotation, context.session) 85 | } 86 | .onEach { propertySymbol -> 87 | val hasReadArrayContentAnnotation = propertySymbol.hasAnnotation( 88 | classId = sessionComponent.pokoReadArrayContentAnnotation, 89 | session = context.session, 90 | ) 91 | val propertyType = propertySymbol.resolvedReturnType 92 | if ( 93 | hasReadArrayContentAnnotation && 94 | !propertyType.isArrayOrPrimitiveArray && 95 | !propertyType.mayBeRuntimeArray() 96 | ) { 97 | reporter.reportOn( 98 | source = propertySymbol.source, 99 | factory = Diagnostics.ReadArrayContentOnNonArrayProperty, 100 | ) 101 | } 102 | } 103 | if (filteredConstructorProperties.isEmpty()) { 104 | reporter.reportOn( 105 | source = declaration.source, 106 | factory = Diagnostics.PrimaryConstructorPropertiesRequired, 107 | ) 108 | } 109 | } 110 | 111 | private fun FirRegularClass.hasAnyPokoClassAnnotation( 112 | context: CheckerContext, 113 | ): Boolean { 114 | val sessionComponent = context.session.pokoFirExtensionSessionComponent 115 | return hasAnnotation(sessionComponent.pokoAnnotation) || 116 | hasAnnotation(sessionComponent.pokoEqualsAndHashCodeAnnotation) || 117 | hasAnnotation(sessionComponent.pokoToStringAnnotation) 118 | } 119 | 120 | private fun FirDeclaration.hasAnnotation( 121 | annotation: ClassId, 122 | ): Boolean { 123 | return annotations.any { firAnnotation -> 124 | firAnnotation.classId() == annotation 125 | } 126 | } 127 | 128 | private fun FirAnnotation.classId(): ClassId? { 129 | return annotationTypeRef.coneTypeOrNull?.classId 130 | } 131 | 132 | /** 133 | * Returns true if the property represents a type that may be an array at runtime (e.g. 134 | * [Any] or a generic type). 135 | */ 136 | context(context: CheckerContext) 137 | private fun ConeKotlinType.mayBeRuntimeArray(): Boolean { 138 | val builtinTypes = context.session.builtinTypes 139 | return this == builtinTypes.anyType.coneType || 140 | this == builtinTypes.nullableAnyType.coneType || 141 | (this is ConeTypeParameterType && hasArrayOrPrimitiveArrayUpperBound()) 142 | } 143 | 144 | context(context: CheckerContext) 145 | private fun ConeTypeParameterType.hasArrayOrPrimitiveArrayUpperBound(): Boolean { 146 | val builtinTypes = context.session.builtinTypes 147 | lookupTag.typeParameterSymbol.resolvedBounds.forEach { resolvedBound -> 148 | val resolvedBoundConeType = resolvedBound.coneType 149 | // Note: A generic type cannot have an array as an upper bound, else that would also be 150 | // checked here. 151 | val foundUpperBoundMatch = resolvedBoundConeType == builtinTypes.anyType.coneType || 152 | resolvedBoundConeType == builtinTypes.nullableAnyType.coneType || 153 | (resolvedBoundConeType is ConeTypeParameterType && 154 | resolvedBoundConeType.hasArrayOrPrimitiveArrayUpperBound()) 155 | 156 | if (foundUpperBoundMatch) { 157 | return true 158 | } 159 | } 160 | 161 | return false 162 | } 163 | } 164 | 165 | private object Diagnostics : KtDiagnosticsContainer() { 166 | val PokoOnNonClass by error0( 167 | positioningStrategy = SourceElementPositioningStrategies.NAME_IDENTIFIER, 168 | ) 169 | 170 | val PokoOnDataClass by error0( 171 | positioningStrategy = SourceElementPositioningStrategies.DATA_MODIFIER, 172 | ) 173 | 174 | val PokoOnValueClass by error0( 175 | positioningStrategy = SourceElementPositioningStrategies.INLINE_OR_VALUE_MODIFIER, 176 | ) 177 | 178 | val PokoOnInnerClass by error0( 179 | positioningStrategy = SourceElementPositioningStrategies.INNER_MODIFIER, 180 | ) 181 | 182 | val PrimaryConstructorRequired by error0( 183 | positioningStrategy = SourceElementPositioningStrategies.NAME_IDENTIFIER, 184 | ) 185 | 186 | val PrimaryConstructorPropertiesRequired by error0( 187 | positioningStrategy = SourceElementPositioningStrategies.NAME_IDENTIFIER, 188 | ) 189 | 190 | val ReadArrayContentOnNonArrayProperty by error0( 191 | positioningStrategy = SourceElementPositioningStrategies.ANNOTATION_USE_SITE, 192 | ) 193 | 194 | override fun getRendererFactory(): BaseDiagnosticRendererFactory = DiagnosticRendererFactory 195 | } 196 | 197 | private object DiagnosticRendererFactory : BaseDiagnosticRendererFactory() { 198 | override val MAP by KtDiagnosticFactoryToRendererMap("Poko") { 199 | it.put( 200 | factory = Diagnostics.PokoOnNonClass, 201 | message = "Poko can only be applied to a class", 202 | ) 203 | it.put( 204 | factory = Diagnostics.PokoOnDataClass, 205 | message = "Poko cannot be applied to a data class", 206 | ) 207 | it.put( 208 | factory = Diagnostics.PokoOnValueClass, 209 | message = "Poko cannot be applied to a value class", 210 | ) 211 | it.put( 212 | factory = Diagnostics.PokoOnInnerClass, 213 | message = "Poko cannot be applied to an inner class" 214 | ) 215 | it.put( 216 | factory = Diagnostics.PrimaryConstructorRequired, 217 | message = "Poko class must have a primary constructor" 218 | ) 219 | it.put( 220 | factory = Diagnostics.PrimaryConstructorPropertiesRequired, 221 | message = "Poko class primary constructor must have at least one not-skipped property", 222 | ) 223 | it.put( 224 | factory = Diagnostics.ReadArrayContentOnNonArrayProperty, 225 | message = "@ReadArrayContent is only supported on properties with array type or `Any` type" 226 | ) 227 | } 228 | } 229 | } 230 | --------------------------------------------------------------------------------