From 4172b441c6e164fb11a4c6d16a9fa655438ffb10 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 21:55:23 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=86=95=20Add=20support=20for=20extens?= =?UTF-8?q?ions=20on=20the=20companion=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sealed/SealedObjectInstancesProcessor.kt | 69 ++++++++++++++----- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/processor/src/main/kotlin/fr/smarquis/sealed/SealedObjectInstancesProcessor.kt b/processor/src/main/kotlin/fr/smarquis/sealed/SealedObjectInstancesProcessor.kt index 1ab6994..0a621cc 100644 --- a/processor/src/main/kotlin/fr/smarquis/sealed/SealedObjectInstancesProcessor.kt +++ b/processor/src/main/kotlin/fr/smarquis/sealed/SealedObjectInstancesProcessor.kt @@ -52,32 +52,54 @@ internal class SealedObjectInstancesProcessor( override fun process(resolver: Resolver): List { resolver.getSymbolsWithAnnotation(SealedObjectInstances::class.qualifiedName!!) .filterIsInstance() - .filter { SEALED in it.modifiers } - .forEach(::process) + .groupBy { it.sealedClass() } + .filterNotNullValues() + .mapValues { it.annotations() } + .forEach { it.process() } return emptyList() } - private fun process(declaration: KSClassDeclaration) = declaration.annotations() + /** + * @return the corresponding sealed class declaration for the [this] declaration. + */ + private fun KSClassDeclaration.sealedClass() = when { + SEALED in modifiers -> this + isCompanionObject -> parentDeclaration as KSClassDeclaration + else -> environment.logger.error( + message = "Failed to find a corresponding sealed class!", + symbol = this, + ).let { null } + } + + @Suppress("UNCHECKED_CAST") + private fun Map.filterNotNullValues(): Map = filterKeys { it != null } as Map + + /** + * @return the [List] of [SealedObjectInstances] annotations for [this] entry. + */ + @OptIn(KspExperimental::class) + private fun Map.Entry>.annotations() = value + .flatMap { it.getAnnotationsByType(SealedObjectInstances::class) } + .also { + if (it.size == it.distinctBy(SealedObjectInstances::name).size) return@also + environment.logger.error( + message = "Duplicated names: ${it.groupingBy(SealedObjectInstances::name).eachCount()}", + symbol = key, + ) + } + + private fun Map.Entry>.process() = value .groupBy { it.fileName.takeUnless(String::isBlank) } - .mapKeys { declaration.createNewFile(it.key) } + .mapKeys { key.createNewFile(it.key) } .forEach { (file, annotations) -> file.writer().use { - it.appendHeader(declaration) + it.appendHeader(key) annotations.forEach { annotation -> - it.appendMethod(declaration, annotation) + it.appendMethod(key, annotation) } } } - @OptIn(KspExperimental::class) - private fun KSClassDeclaration.annotations() = getAnnotationsByType(SealedObjectInstances::class).toList().also { - if (it.size == it.distinctBy(SealedObjectInstances::name).size) return@also - environment.logger.error( - message = "Duplicated names: ${it.groupingBy(SealedObjectInstances::name).eachCount()}", - symbol = this, - ) - } - private fun KSClassDeclaration.createNewFile(fileName: String?) = runCatching { environment.codeGenerator.createNewFile( dependencies = Dependencies( @@ -118,13 +140,28 @@ internal class SealedObjectInstancesProcessor( postfix = ")", ) { it.qualifiedName!!.asString() } - // language=kotlin + // language=kotlin ~ Extension on the KClass """ /** @return [$rawClassName] of sealed object instances of type [$sealedClassName]. */ ${visibility.modifier()} fun $receiverType.$methodName()$returnType = $collectionBuilder """.trimIndent().let(::appendLine) + + sealed.companionOrNull()?.let { + // language=kotlin ~ Extension on the companion object + """ + /** @return [$rawClassName] of sealed object instances of type [$sealedClassName]. */ + ${visibility.modifier()} fun $sealedClassName$genericReceiverType.$it.$methodName()$returnType = $sealedClassName$genericReceiverType::class.$methodName() + """.trimIndent().let(::appendLine) + } } + /** + * @return the companion object of [this] declaration or `null` if it does not exist. + */ + private fun KSClassDeclaration.companionOrNull() = declarations + .filterIsInstance() + .singleOrNull { it.isCompanionObject } + private fun KSClassDeclaration.getVisibility( annotation: SealedObjectInstances, ): Visibility = when (val visibility = annotation.visibility) { From 6ab41ad1214ab4ed89e9cb33e869dd40a5020308 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 22:09:18 +0200 Subject: [PATCH 2/7] Add tests for companion objects --- .../fr/smarquis/sealed/CompanionObjectTest.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt diff --git a/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt b/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt new file mode 100644 index 0000000..c28ac4e --- /dev/null +++ b/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 Simon Marquis + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fr.smarquis.sealed + +import kotlin.test.Test +import kotlin.test.assertSame + +class CompanionObjectTest { + + @SealedObjectInstances + sealed class AnnotatedSealedClassWithCompanionObject { + companion object + object INSTANCE : AnnotatedSealedClassWithCompanionObject() + } + + @Test + fun testAnnotatedSealedClassWithCompanionObject() = assertSame( + expected = AnnotatedSealedClassWithCompanionObject::class.sealedObjectInstances().single(), + actual = AnnotatedSealedClassWithCompanionObject.sealedObjectInstances().single(), + ) + + sealed class SealedClassWithAnnotatedCompanionObject { + @SealedObjectInstances companion object + object INSTANCE : SealedClassWithAnnotatedCompanionObject() + } + + @Test + fun testSealedClassWithAnnotatedCompanionObject() = assertSame( + expected = SealedClassWithAnnotatedCompanionObject::class.sealedObjectInstances().single(), + actual = SealedClassWithAnnotatedCompanionObject.sealedObjectInstances().single(), + ) + + @SealedObjectInstances(name = "onSealedClass") + sealed class SealedClassWithCompanionObjectBothAnnotated { + @SealedObjectInstances(name = "onCompanionObject") + companion object + object INSTANCE : SealedClassWithCompanionObjectBothAnnotated() + } + + @Test + fun testSealedClassWithCompanionObjectBothAnnotated() = assertSame( + expected = SealedClassWithAnnotatedCompanionObject::class.sealedObjectInstances().single(), + actual = SealedClassWithAnnotatedCompanionObject.sealedObjectInstances().single(), + ) + + sealed class SealedClassWithNameCompanionObject { + @SealedObjectInstances companion object Foo + object INSTANCE : SealedClassWithNameCompanionObject() + } + + @Test + fun testSealedClassWithNameCompanionObject() = assertSame( + expected = SealedClassWithNameCompanionObject::class.sealedObjectInstances().single(), + actual = SealedClassWithNameCompanionObject.sealedObjectInstances().single(), + ) + +} From f26b0d0f770e2ac78c765b1160ec37ab9043658b Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 22:10:05 +0200 Subject: [PATCH 3/7] Update editor config to wrap class annotation only if too long --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index ba2a7f9..8b23202 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,7 @@ ij_kotlin_allow_trailing_comma_on_call_site = true ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 ij_kotlin_packages_to_use_import_on_demand = unset +ij_kotlin_class_annotation_wrap = normal [{*.md,*.mdown,*.markdown}] indent_size = 2 From 533b3292b3869203c65930bfb004d900ebdd36ed Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 22:16:41 +0200 Subject: [PATCH 4/7] Update tests to use the companion extension where it is simpler --- app/src/main/kotlin/fr/smarquis/sealed/Debug.kt | 2 +- app/src/main/kotlin/fr/smarquis/sealed/FeatureFlag.kt | 2 +- app/src/main/kotlin/fr/smarquis/sealed/UI.kt | 2 +- .../fr/smarquis/sealed/NestedExtensionReferencesTest.kt | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/fr/smarquis/sealed/Debug.kt b/app/src/main/kotlin/fr/smarquis/sealed/Debug.kt index a17dd97..1a21527 100644 --- a/app/src/main/kotlin/fr/smarquis/sealed/Debug.kt +++ b/app/src/main/kotlin/fr/smarquis/sealed/Debug.kt @@ -15,8 +15,8 @@ */ package fr.smarquis.sealed -@SealedObjectInstances sealed class Debug(override val isEnabled: Boolean = false) : FeatureFlag() { + @SealedObjectInstances companion object object Logs : Debug(true) object Traces : Debug() object StrictMode : Debug() diff --git a/app/src/main/kotlin/fr/smarquis/sealed/FeatureFlag.kt b/app/src/main/kotlin/fr/smarquis/sealed/FeatureFlag.kt index 8e3986a..04d391a 100644 --- a/app/src/main/kotlin/fr/smarquis/sealed/FeatureFlag.kt +++ b/app/src/main/kotlin/fr/smarquis/sealed/FeatureFlag.kt @@ -15,7 +15,7 @@ */ package fr.smarquis.sealed -@SealedObjectInstances sealed class FeatureFlag { + @SealedObjectInstances companion object; abstract val isEnabled: Boolean } diff --git a/app/src/main/kotlin/fr/smarquis/sealed/UI.kt b/app/src/main/kotlin/fr/smarquis/sealed/UI.kt index c487161..3b1ab27 100644 --- a/app/src/main/kotlin/fr/smarquis/sealed/UI.kt +++ b/app/src/main/kotlin/fr/smarquis/sealed/UI.kt @@ -15,8 +15,8 @@ */ package fr.smarquis.sealed -@SealedObjectInstances sealed class UI(override val isEnabled: Boolean = false) : FeatureFlag() { + @SealedObjectInstances companion object object Animations : UI() object Framerate : UI() } diff --git a/processor/src/test/kotlin/fr/smarquis/sealed/NestedExtensionReferencesTest.kt b/processor/src/test/kotlin/fr/smarquis/sealed/NestedExtensionReferencesTest.kt index 757afb5..e0dde88 100644 --- a/processor/src/test/kotlin/fr/smarquis/sealed/NestedExtensionReferencesTest.kt +++ b/processor/src/test/kotlin/fr/smarquis/sealed/NestedExtensionReferencesTest.kt @@ -24,14 +24,14 @@ class NestedExtensionReferencesTest { sealed class Sealed { object Object : Sealed() companion object { - val values get() = Sealed::class.sealedObjectInstances() - val lazyValues by lazy(Sealed::class::sealedObjectInstances) + val values get() = Sealed.sealedObjectInstances() + val lazyValues by lazy(Companion::sealedObjectInstances) } } @Test fun `referencing extension from within the class compiles and returns the same values`() { - assertEquals(Sealed::class.sealedObjectInstances(), Sealed.values) - assertEquals(Sealed::class.sealedObjectInstances(), Sealed.lazyValues) + assertEquals(Sealed.sealedObjectInstances(), Sealed.values) + assertEquals(Sealed.sealedObjectInstances(), Sealed.lazyValues) } } From 35a6cf7795b676bad1897ac147408d9d35a94054 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 22:31:40 +0200 Subject: [PATCH 5/7] Add documentation for the new companion object extensions --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 85e8335..0000207 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,17 @@ And then you'll have access to the `sealedObjectInstances()` extension on the co val flags: Set = FeatureFlag::class.sealedObjectInstances() ``` +> **Note** +> Alternatively, you can annotate the `companion object` to access a simpler extension function (no more `::class` prefix). +> ```kotlin +> sealed class FeatureFlag { +> @SealedObjectInstances companion object +> /*...*/ +> } +> +> val flags: Set = FeatureFlag.sealedObjectInstances() +> ``` + ## Setup In the module's build script, apply the `com.google.devtools.ksp` plugin with the current Kotlin version: From 248e4b63e3ea720ae6e9bb3c41aec2af9f42b315 Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 22:36:55 +0200 Subject: [PATCH 6/7] `spotlessApply` --- .../src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt b/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt index c28ac4e..bfe0df8 100644 --- a/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt +++ b/processor/src/test/kotlin/fr/smarquis/sealed/CompanionObjectTest.kt @@ -66,5 +66,4 @@ class CompanionObjectTest { expected = SealedClassWithNameCompanionObject::class.sealedObjectInstances().single(), actual = SealedClassWithNameCompanionObject.sealedObjectInstances().single(), ) - } From 969764766ba117f116956a2bf4b43ed097cbecff Mon Sep 17 00:00:00 2001 From: Simon Marquis Date: Sat, 22 Apr 2023 22:47:08 +0200 Subject: [PATCH 7/7] Force line break --- README.md | 4 ++-- build.gradle.kts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0000207..abb80e5 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ And then you'll have access to the `sealedObjectInstances()` extension on the co val flags: Set = FeatureFlag::class.sealedObjectInstances() ``` -> **Note** -> Alternatively, you can annotate the `companion object` to access a simpler extension function (no more `::class` prefix). +> **Note** +> You can annotate the `companion object` to access a simpler extension function (no more `::class` prefix). > ```kotlin > sealed class FeatureFlag { > @SealedObjectInstances companion object diff --git a/build.gradle.kts b/build.gradle.kts index b3dff37..cecd1e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,6 @@ allprojects { val licenseHeader = rootProject.file("spotless/spotless.kt") format("misc") { target("**/*.md", "**/.gitignore") - trimTrailingWhitespace() endWithNewline() } kotlin {