├── .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 |
4 |
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 |
5 |
6 |
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 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 | [](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 | [](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 |
--------------------------------------------------------------------------------