Repository: yandex/yatagan (kotlin)

Issue:
Ensure every annotation processing option in declared in kapt/apt
It'd be good to have all available [options](https://github.com/yandex/yatagan/blob/67fe0f30c4b5e7626e85429c3bea78bc46f58ceb/processor/common/src/main/kotlin/Options.kt#L59) automatically declared in [kapt processor](https://github.com/yandex/yatagan/blob/67fe0f30c4b5e7626e85429c3bea78bc46f58ceb/processor/jap/src/main/kotlin/JapYataganProcessor.kt#L27).
This would prevent warnings about unused options on the client's side.

Pull request description:
I hereby agree to the terms of the CLA available at: https://yandex.ru/legal/cla/?lang=ru.

Resolves #15.

Patch:
diff --git a/processor/common/build.gradle.kts b/processor/common/build.gradle.kts
index 365e041a..3b25db3d 100644
--- a/processor/common/build.gradle.kts
+++ b/processor/common/build.gradle.kts
@@ -9,4 +9,6 @@ dependencies {
     implementation(project(":core:graph:impl"))
     implementation(project(":core:model:impl"))
     implementation(project(":codegen:impl"))
+
+    runtimeOnly(kotlin("reflect"))
 }
\ No newline at end of file
diff --git a/processor/common/src/main/kotlin/Options.kt b/processor/common/src/main/kotlin/Options.kt
index f8940b46..dcaacb05 100644
--- a/processor/common/src/main/kotlin/Options.kt
+++ b/processor/common/src/main/kotlin/Options.kt
@@ -16,54 +16,65 @@
 
 package com.yandex.yatagan.processor.common
 
-import kotlin.reflect.KProperty
-
 /**
  * A wrapper around a simple options map that provides safe option accessing API.
  */
 class Options(
     private val values: Map<String, String>,
 ) {
-    operator fun get(option: BooleanOption): Boolean {
-        val value = values[option.key] ?: return option.default
-        return when (value.lowercase()) {
-            "yes", "enabled", "enable", "true" -> true
-            "no", "disabled", "disable", "false" -> false
-            else -> throw RuntimeException("Invalid boolean option value: $value")
-        }
-    }
-
-    operator fun get(option: IntOption): Int {
+    operator fun <T> get(option: Option<T>): T {
         val value = values[option.key] ?: return option.default
-        return value.toIntOrNull() ?: throw RuntimeException("Invalid integer option value: $value")
+        return option.parse(value)
     }
 
-    class BooleanOption internal constructor(
-        val key: String,
-        val default: Boolean,
-    ) {
-        operator fun getValue(delegate: ProcessorDelegate<*>, property: KProperty<*>): Boolean {
-            return delegate.options[this]
+    companion object {
+        fun all(): Set<Option<*>> = buildSet {
+            @Suppress("NO_REFLECTION_IN_CLASS_PATH")
+            for (sealedSubclass in Option::class.sealedSubclasses) {
+                check(sealedSubclass.java.isEnum) { "Unexpected option class" }
+                addAll(sealedSubclass.java.enumConstants)
+            }
         }
     }
+}
 
-    class IntOption internal constructor(
-        val key: String,
-        val default: Int,
-    ) {
-        operator fun getValue(delegate: ProcessorDelegate<*>, property: KProperty<*>): Int {
-            return delegate.options[this]
+sealed interface Option<out T> {
+    val key: String
+    val default: T
+    fun parse(rawValue: String): T
+}
+
+enum class BooleanOption(
+    override val key: String,
+    override val default: Boolean,
+) : Option<Boolean> {
+
+    StrictMode("yatagan.enableStrictMode", default = true),
+    UsePlainOutput("yatagan.usePlainOutput", default = false),
+
+    ;
+
+    override fun parse(rawValue: String): Boolean {
+        return when (rawValue.lowercase()) {
+            "yes", "enabled", "enable", "true" -> true
+            "no", "disabled", "disable", "false" -> false
+            else -> throw RuntimeException("Invalid boolean option value: $rawValue")
         }
     }
+}
 
-    companion object {
-        // TODO: Hook with docs
-        val StrictMode = BooleanOption("yatagan.enableStrictMode", default = true)
+enum class IntOption(
+    override val key: String,
+    override val default: Int,
+) : Option<Int> {
 
-        val MaxIssueEncounterPaths = IntOption("yatagan.maxIssueEncounterPaths", default = 5)
+    MaxIssueEncounterPaths("yatagan.maxIssueEncounterPaths", default = 5),
+    MaxSlotsPerSwitch("yatagan.experimental.maxSlotsPerSwitch", default = -1),
 
-        val UsePlainOutput = BooleanOption("yatagan.usePlainOutput", default = false)
+    ;
 
-        val MaxSlotsPerSwitch = IntOption("yatagan.experimental.maxSlotsPerSwitch", default = -1)
+    override fun parse(rawValue: String): Int {
+        return rawValue.toIntOrNull() ?: throw RuntimeException("Invalid integer option value: $rawValue")
     }
-}
\ No newline at end of file
+}
+
diff --git a/processor/common/src/main/kotlin/process.kt b/processor/common/src/main/kotlin/process.kt
index 509e6889..0dd19185 100644
--- a/processor/common/src/main/kotlin/process.kt
+++ b/processor/common/src/main/kotlin/process.kt
@@ -38,8 +38,8 @@ fun <Source> process(
     sources: Sequence<Source>,
     delegate: ProcessorDelegate<Source>,
 ) {
-    val usePlainOutput = delegate.options[Options.UsePlainOutput]
-    val strictMode = delegate.options[Options.StrictMode]
+    val usePlainOutput = delegate.options[BooleanOption.UsePlainOutput]
+    val strictMode = delegate.options[BooleanOption.StrictMode]
     ObjectCacheRegistry.use {
         val rootModels = sources.mapNotNull { source ->
             ComponentModel(delegate.createDeclaration(source))
@@ -62,7 +62,7 @@ fun <Source> process(
 
             allMessages.forEach { locatedMessage ->
                 val message = locatedMessage.format(
-                    maxEncounterPaths = delegate.options[Options.MaxIssueEncounterPaths],
+                    maxEncounterPaths = delegate.options[IntOption.MaxIssueEncounterPaths],
                 ).run {
                     if (usePlainOutput) toString() else toAnsiEscapedString()
                 }
@@ -85,7 +85,7 @@ fun <Source> process(
             try {
                 val generator = ComponentGeneratorFacade(
                     graph = graphRoot,
-                    maxSlotsPerSwitch = delegate.options[Options.MaxSlotsPerSwitch],
+                    maxSlotsPerSwitch = delegate.options[IntOption.MaxSlotsPerSwitch],
                 )
                 delegate.openFileForGenerating(
                     sources = allSourcesSequence(delegate, graphRoot),
diff --git a/processor/jap/src/main/kotlin/JapYataganProcessor.kt b/processor/jap/src/main/kotlin/JapYataganProcessor.kt
index 3b9798ab..e7a58605 100644
--- a/processor/jap/src/main/kotlin/JapYataganProcessor.kt
+++ b/processor/jap/src/main/kotlin/JapYataganProcessor.kt
@@ -24,11 +24,7 @@ import javax.lang.model.SourceVersion
 @SupportedSourceVersion(SourceVersion.RELEASE_8)
 class JapYataganProcessor : BasicAnnotationProcessor() {
     override fun getSupportedOptions(): Set<String> {
-        return setOf(
-            Options.StrictMode.key,
-            Options.MaxIssueEncounterPaths.key,
-            Options.UsePlainOutput.key,
-        )
+        return Options.all().mapTo(mutableSetOf()) { it.key }
     }
 
     override fun steps(): Iterable<Step> = with(processingEnv) {


Test patch:
diff --git a/testing/tests/src/main/kotlin/CompileTestDriverBase.kt b/testing/tests/src/main/kotlin/CompileTestDriverBase.kt
index 7c7f009c..6aa6b3b8 100644
--- a/testing/tests/src/main/kotlin/CompileTestDriverBase.kt
+++ b/testing/tests/src/main/kotlin/CompileTestDriverBase.kt
@@ -20,8 +20,8 @@ import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
 import androidx.room.compiler.processing.util.compiler.compile
 import com.yandex.yatagan.generated.CompiledApiClasspath
 import com.yandex.yatagan.generated.DynamicApiClasspath
+import com.yandex.yatagan.processor.common.IntOption
 import com.yandex.yatagan.processor.common.LoggerDecorator
-import com.yandex.yatagan.processor.common.Options
 import com.yandex.yatagan.testing.source_set.SourceFile
 import com.yandex.yatagan.testing.source_set.SourceSet
 import org.junit.Assert
@@ -162,8 +162,8 @@ abstract class CompileTestDriverBase private constructor(
             "-opt-in=com.yandex.yatagan.VariantApi",
         ),
         processorOptions = mapOf(
-            Options.MaxIssueEncounterPaths.key to "100",
-            Options.MaxSlotsPerSwitch.key to "100",
+            IntOption.MaxIssueEncounterPaths.key to "100",
+            IntOption.MaxSlotsPerSwitch.key to "100",
         ),
     )