From 74ab3d8ab4b717556b977d4e3f9f909ced96ce78 Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sun, 17 May 2026 12:55:51 +0300 Subject: [PATCH 1/2] Add enum-typed flags to featured-gradle-plugin DSL (#162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `FlagContainer.enum(key, typeFqn, default)` declarator. Enum flags round-trip through FlagSpec/ResolveFlagsTask as the fully-qualified class name stored in the `type` field. LocalFlagEntry.isEnum detects enum types via presence of '.' in the type string. Generator behaviour: - ConfigParamGenerator: emits ConfigParam with default expressed as FQN.CONSTANT_NAME. - ExtensionFunctionGenerator: emits get…() extension returning the raw enum type (same as non-Boolean primitives). - ProguardRulesGenerator: skips enum flags — values are resolved at runtime and cannot be assumed at build time. - IosConstValGenerator: skips enum flags — const val only supports primitives and String. Integration test fixture: adds CheckoutVariant enum source so assembleRelease compiles generated ConfigParam. assertContainsAssumevaluesBlock now also asserts enum flags are absent from ProGuard output. --- .../featured/gradle/ConfigParamGenerator.kt | 9 +-- .../featured/gradle/FlagContainer.kt | 21 ++++++ .../featured/gradle/FlagEntryUtils.kt | 2 +- .../featured/gradle/FlagSpec.kt | 3 +- .../featured/gradle/IosConstValGenerator.kt | 23 +++++-- .../featured/gradle/LocalFlagEntry.kt | 14 +++- .../featured/gradle/ProguardRulesGenerator.kt | 4 ++ .../fixtures/android-project/build.gradle.kts | 1 + .../featured/testapp/CheckoutVariant.kt | 6 ++ .../gradle/ConfigParamGeneratorTest.kt | 23 +++++++ .../gradle/ExtensionFunctionGeneratorTest.kt | 17 +++++ .../gradle/FeaturedPluginIntegrationTest.kt | 10 ++- .../featured/gradle/FlagContainerTest.kt | 38 +++++++++++ .../gradle/IosConstValGeneratorTest.kt | 67 +++++++++++++++++++ .../gradle/ProguardRulesGeneratorTest.kt | 21 ++++++ 15 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt index 2f21570..f698f90 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGenerator.kt @@ -63,10 +63,11 @@ public object ConfigParamGenerator { } private fun LocalFlagEntry.formatDefault(): String = - when (type) { - "String" -> defaultValue - "Long" -> "${defaultValue}L" - "Float" -> "${defaultValue}f" + when { + isEnum -> "$type.$defaultValue" + type == "String" -> defaultValue + type == "Long" -> "${defaultValue}L" + type == "Float" -> "${defaultValue}f" else -> defaultValue } } diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt index 9ab01cb..ea3595c 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt @@ -10,6 +10,7 @@ package dev.androidbroadcast.featured.gradle * boolean("dark_mode", default = false) { category = "UI" } * int("retry_count", default = 3) * string("api_base_url", default = "https://example.com") + * enum("checkout_variant", typeFqn = "com.example.CheckoutVariant", default = "LEGACY") * } * remoteFlags { * boolean("promo_banner_enabled", default = false) { @@ -80,6 +81,26 @@ public class FlagContainer { _flags += FlagSpec(key, "\"$default\"", "String").apply(configure) } + /** + * Declares an enum-typed feature flag. + * + * Enum flags are intentionally excluded from R8 `-assumevalues` DCE rules — the value + * cannot be assumed at build time (it is resolved at runtime from providers). + * + * @param key The configuration key string (e.g. `"checkout_variant"`). + * @param typeFqn The fully-qualified Kotlin class name of the enum (e.g. `"com.example.CheckoutVariant"`). + * @param default The name of the default enum constant (e.g. `"LEGACY"`). + * @param configure Optional block to set [FlagSpec.description], [FlagSpec.category], or [FlagSpec.expiresAt]. + */ + public fun enum( + key: String, + typeFqn: String, + default: String, + configure: FlagSpec.() -> Unit = {}, + ) { + _flags += FlagSpec(key = key, defaultValue = default, type = typeFqn).apply(configure) + } + /** Serialises all flags to pipe-delimited descriptors for [ResolveFlagsTask] inputs. */ internal fun toDescriptors(): List = _flags.map { it.toDescriptor() } } diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt index db81d9a..b1f93fe 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagEntryUtils.kt @@ -36,7 +36,7 @@ internal fun String.capitalized(): String = replaceFirstChar { it.uppercase() } * Returns the name of the generated `ConfigValues` extension function for this flag. * * - Boolean flags: `isEnabled` (e.g. `isDarkModeEnabled`) - * - All other types: `get` (e.g. `getMaxRetries`) + * - All other types (including enum): `get` (e.g. `getMaxRetries`, `getCheckoutVariant`) */ internal fun LocalFlagEntry.extensionFunctionName(): String { val capitalized = propertyName.capitalized() diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt index 8668f10..8e6d0b9 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagSpec.kt @@ -5,7 +5,8 @@ package dev.androidbroadcast.featured.gradle * * @property key The configuration key string (e.g. `"dark_mode"`). Acts as the unique identifier. * @property defaultValue The default value serialised to a string (e.g. `"false"`, `"42"`). - * @property type The Kotlin type name: `"Boolean"`, `"Int"`, `"Long"`, `"Float"`, `"Double"`, or `"String"`. + * @property type The Kotlin type name: `"Boolean"`, `"Int"`, `"Long"`, `"Float"`, `"Double"`, `"String"`, + * or a fully-qualified enum class name (e.g. `"com.example.CheckoutVariant"`). * @property description Optional human-readable description passed to the generated [ConfigParam]. * @property category Optional grouping label shown in the debug UI. * @property expiresAt Optional ISO-8601 date (`"YYYY-MM-DD"`) after which the flag is considered stale. diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt index 7461009..0a7c6ce 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGenerator.kt @@ -15,21 +15,28 @@ private const val HEADER = "// Auto-generated by featured-gradle-plugin — do n * * String values are wrapped in double-quotes; all other primitive types are emitted * verbatim (matching Kotlin literal syntax for Boolean, Int, Long, Double, Float). + * + * **Enum flags are not supported** — Kotlin `const val` only accepts primitive types + * and `String`. Enum entries are silently skipped in both [generate] and [generateExpect]. */ public object IosConstValGenerator { /** * Generates the `iosMain` Kotlin source file containing `actual const val` * declarations for every entry in [entries]. * - * Returns a blank string when [entries] is empty. + * Enum-typed entries (detected via [LocalFlagEntry.isEnum]) are excluded because + * `const val` does not support enum types. + * + * Returns a blank string when [entries] is empty or all entries are enums. */ public fun generate(entries: List): String { - if (entries.isEmpty()) return "" + val eligible = entries.filterNot { it.isEnum } + if (eligible.isEmpty()) return "" return buildString { appendLine(HEADER) appendLine("package $GENERATED_PACKAGE") appendLine() - entries.forEach { entry -> + eligible.forEach { entry -> appendLine(actualDeclaration(entry)) } } @@ -39,15 +46,19 @@ public object IosConstValGenerator { * Generates the `commonMain` Kotlin source file containing `expect val` * declarations for every entry in [entries]. * - * Returns a blank string when [entries] is empty. + * Enum-typed entries (detected via [LocalFlagEntry.isEnum]) are excluded because + * `const val` does not support enum types. + * + * Returns a blank string when [entries] is empty or all entries are enums. */ public fun generateExpect(entries: List): String { - if (entries.isEmpty()) return "" + val eligible = entries.filterNot { it.isEnum } + if (eligible.isEmpty()) return "" return buildString { appendLine(HEADER) appendLine("package $GENERATED_PACKAGE") appendLine() - entries.forEach { entry -> + eligible.forEach { entry -> appendLine("public expect val ${entry.key}: ${entry.type}") } } diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt index b6e467e..503b09a 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt @@ -5,7 +5,8 @@ package dev.androidbroadcast.featured.gradle * * @property key The configuration key string (e.g. `"dark_mode"`). * @property defaultValue The default value as a raw string (e.g. `"false"`, `"42"`). - * @property type The Kotlin type name: `"Boolean"`, `"Int"`, `"Long"`, `"Float"`, `"Double"`, or `"String"`. + * @property type The Kotlin type name: `"Boolean"`, `"Int"`, `"Long"`, `"Float"`, `"Double"`, `"String"`, + * or a fully-qualified enum class name (e.g. `"com.example.CheckoutVariant"`). * @property moduleName The Gradle module path that declares this flag (e.g. `":feature:checkout"`). * @property propertyName The camelCase property name derived from [key] (e.g. `"darkMode"`). * @property flagType Either `"local"` or `"remote"`. @@ -26,6 +27,17 @@ public data class LocalFlagEntry( ) { public val isLocal: Boolean get() = flagType == FLAG_TYPE_LOCAL + /** + * Returns `true` when this flag's type is an enum (i.e. a fully-qualified class name + * containing a `.`) rather than a built-in Kotlin primitive or `String`. + * + * Enum flags require special handling in code generators: + * - ProGuard `-assumevalues` rules are skipped (enum values are not assumable at build time). + * - iOS `const val` generation is skipped (`const` only supports primitive types and `String`). + * - The generated `ConfigParam` default expression uses `TypeFqn.CONSTANT_NAME` syntax. + */ + public val isEnum: Boolean get() = '.' in type + /** * Returns the Kotlin reference used in the generated `FlagRegistry.register(...)` call. * diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt index 24289b0..d1961a3 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt @@ -18,6 +18,10 @@ package dev.androidbroadcast.featured.gradle * cannot be assumed at build time. * * Supported return types: `boolean`, `int`, `long`, `float`, `double`, `java.lang.String`. + * + * **Enum flags are intentionally excluded** — their runtime values are resolved from providers + * and cannot be assumed at build time (see issue #162). [jvmType] returns `null` for any + * unrecognised type (including enum FQNs), which causes those entries to be filtered out. */ public object ProguardRulesGenerator { private const val PACKAGE = "dev.androidbroadcast.featured.generated" diff --git a/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts b/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts index b0725d4..c547743 100644 --- a/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts +++ b/featured-gradle-plugin/src/test/fixtures/android-project/build.gradle.kts @@ -25,5 +25,6 @@ android { featured { localFlags { boolean("dark_mode", default = false) + enum("checkout_variant", typeFqn = "dev.androidbroadcast.featured.testapp.CheckoutVariant", default = "LEGACY") } } diff --git a/featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt b/featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt new file mode 100644 index 0000000..b9bfec0 --- /dev/null +++ b/featured-gradle-plugin/src/test/fixtures/android-project/src/main/kotlin/dev/androidbroadcast/featured/testapp/CheckoutVariant.kt @@ -0,0 +1,6 @@ +package dev.androidbroadcast.featured.testapp + +enum class CheckoutVariant { + LEGACY, + NEW, +} diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt index 09ee258..1ea6cb5 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ConfigParamGeneratorTest.kt @@ -129,6 +129,29 @@ class ConfigParamGeneratorTest { assertContains(local, "Auto-generated by Featured Gradle Plugin") } + // ── enum flags ──────────────────────────────────────────────────────────── + + @Test + fun `generates enum ConfigParam with fqn type argument`() { + val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) + val (local, _) = ConfigParamGenerator.generate(entries) + assertContains(local, "ConfigParam") + } + + @Test + fun `enum default value uses fqn dot constant syntax`() { + val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) + val (local, _) = ConfigParamGenerator.generate(entries) + assertContains(local, "defaultValue = com.example.CheckoutVariant.LEGACY") + } + + @Test + fun `enum flag is included in local object`() { + val entries = listOf(localEntry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) + val (local, _) = ConfigParamGenerator.generate(entries) + assertContains(local, "val checkoutVariant = ConfigParam") + } + // ── helpers ─────────────────────────────────────────────────────────────── private fun localEntry( diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt index 40fcf07..ad2e938 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ExtensionFunctionGeneratorTest.kt @@ -78,6 +78,23 @@ class ExtensionFunctionGeneratorTest { assertContains(source, "fun ConfigValues.getApiUrl(): String") } + // ── local enum flag ─────────────────────────────────────────────────────── + + @Test + fun `generates get… extension for local enum flag`() { + val entries = listOf(localEntry("checkout_variant", "com.example.CheckoutVariant")) + val source = ExtensionFunctionGenerator.generate(entries, modulePath) + assertContains(source, "fun ConfigValues.getCheckoutVariant(): com.example.CheckoutVariant") + assertContains(source, "getValue(GeneratedLocalFlags.checkoutVariant).value") + } + + @Test + fun `enum extension uses get… prefix, not is…Enabled`() { + val entries = listOf(localEntry("checkout_variant", "com.example.CheckoutVariant")) + val source = ExtensionFunctionGenerator.generate(entries, modulePath) + assertFalse(source.contains("isCheckoutVariantEnabled"), "Enum flag must not use is…Enabled naming") + } + // ── remote flag ─────────────────────────────────────────────────────────── @Test diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt index 047cd55..c1d8252 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPluginIntegrationTest.kt @@ -100,7 +100,8 @@ class FeaturedPluginIntegrationTest { /** * Asserts that [content] contains a well-formed `-assumevalues` block targeting the - * extensions class for the root module (`:`) and the `dark_mode` boolean flag. + * extensions class for the root module (`:`) and the `dark_mode` boolean flag, + * and that the enum flag `checkout_variant` is NOT present in the rules. * * Expected output (from [ProguardRulesGenerator]): * ```proguard @@ -111,6 +112,9 @@ class FeaturedPluginIntegrationTest { * * The root module path `:` produces the identifier `Root` via [String.modulePathToIdentifier], * so the JVM class name is `FeaturedRoot_FlagExtensionsKt`. + * + * Enum flags (`checkout_variant`) must not appear in `-assumevalues` rules — their values + * are resolved at runtime from providers and cannot be assumed at build time (issue #162). */ private fun assertContainsAssumevaluesBlock(content: String) { assertTrue( @@ -121,6 +125,10 @@ class FeaturedPluginIntegrationTest { content.contains("boolean $IS_DARK_MODE_ENABLED($CONFIG_VALUES_FQN) return false;"), "Expected 'boolean $IS_DARK_MODE_ENABLED($CONFIG_VALUES_FQN) return false;' in rules\nActual content:\n$content", ) + assertTrue( + !content.contains("checkoutVariant"), + "Enum flag 'checkout_variant' must not appear in -assumevalues rules\nActual content:\n$content", + ) } // ── Helpers ─────────────────────────────────────────────────────────────── diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt index 5737ac3..beaae04 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagContainerTest.kt @@ -101,4 +101,42 @@ class FlagContainerTest { fun `empty container has no flags`() { assertTrue(FlagContainer().flags.isEmpty()) } + + @Test + fun `enum flag stores fully-qualified type name`() { + val container = + FlagContainer().apply { + enum("checkout_variant", typeFqn = "com.example.CheckoutVariant", default = "LEGACY") + } + val flag = container.flags.single() + assertEquals("checkout_variant", flag.key) + assertEquals("com.example.CheckoutVariant", flag.type) + assertEquals("LEGACY", flag.defaultValue) + } + + @Test + fun `enum flag configure block sets description and category`() { + val container = + FlagContainer().apply { + enum("checkout_variant", typeFqn = "com.example.CheckoutVariant", default = "LEGACY") { + description = "Checkout flow variant" + category = "Checkout" + } + } + val flag = container.flags.single() + assertEquals("Checkout flow variant", flag.description) + assertEquals("Checkout", flag.category) + } + + @Test + fun `enum flag descriptor contains type fqn and default constant`() { + val container = + FlagContainer().apply { + enum("checkout_variant", typeFqn = "com.example.CheckoutVariant", default = "LEGACY") + } + val descriptor = container.toDescriptors().single() + assertContains(descriptor, "checkout_variant") + assertContains(descriptor, "com.example.CheckoutVariant") + assertContains(descriptor, "LEGACY") + } } diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt index ba7b6a6..b194aee 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/IosConstValGeneratorTest.kt @@ -2,6 +2,7 @@ package dev.androidbroadcast.featured.gradle import kotlin.test.Test import kotlin.test.assertContains +import kotlin.test.assertFalse import kotlin.test.assertTrue class IosConstValGeneratorTest { @@ -163,4 +164,70 @@ class IosConstValGeneratorTest { val result = IosConstValGenerator.generateExpect(emptyList()) assertTrue(result.isBlank(), "Expected blank output for empty entries, got: '$result'") } + + // ── enum flag exclusion ─────────────────────────────────────────────────── + + @Test + fun `generate skips enum-typed entries`() { + val entries = + listOf( + LocalFlagEntry(key = "dark_mode", defaultValue = "false", type = "Boolean", moduleName = ":app"), + LocalFlagEntry( + key = "checkout_variant", + defaultValue = "LEGACY", + type = "com.example.CheckoutVariant", + moduleName = ":app", + ), + ) + val result = IosConstValGenerator.generate(entries) + assertContains(result, "public actual const val dark_mode: Boolean = false") + assertFalse(result.contains("checkout_variant"), "Enum flags must not appear in iOS const val output") + } + + @Test + fun `generate returns blank when only enum entries are present`() { + val entries = + listOf( + LocalFlagEntry( + key = "checkout_variant", + defaultValue = "LEGACY", + type = "com.example.CheckoutVariant", + moduleName = ":app", + ), + ) + val result = IosConstValGenerator.generate(entries) + assertTrue(result.isBlank(), "Expected blank output when all entries are enums, got: '$result'") + } + + @Test + fun `generateExpect skips enum-typed entries`() { + val entries = + listOf( + LocalFlagEntry(key = "dark_mode", defaultValue = "false", type = "Boolean", moduleName = ":app"), + LocalFlagEntry( + key = "checkout_variant", + defaultValue = "LEGACY", + type = "com.example.CheckoutVariant", + moduleName = ":app", + ), + ) + val result = IosConstValGenerator.generateExpect(entries) + assertContains(result, "public expect val dark_mode: Boolean") + assertFalse(result.contains("checkout_variant"), "Enum flags must not appear in iOS expect declarations") + } + + @Test + fun `generateExpect returns blank when only enum entries are present`() { + val entries = + listOf( + LocalFlagEntry( + key = "checkout_variant", + defaultValue = "LEGACY", + type = "com.example.CheckoutVariant", + moduleName = ":app", + ), + ) + val result = IosConstValGenerator.generateExpect(entries) + assertTrue(result.isBlank(), "Expected blank output when all entries are enums, got: '$result'") + } } diff --git a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt index 98a2ceb..55f7fab 100644 --- a/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt +++ b/featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt @@ -122,6 +122,27 @@ class ProguardRulesGeneratorTest { assertFalse(rules.contains("isRemoteFlagEnabled"), "Remote flags must not appear in ProGuard rules") } + @Test + fun `excludes enum flags from rules`() { + val entries = + listOf( + entry("dark_mode", "false", "Boolean"), + entry("checkout_variant", "LEGACY", "com.example.CheckoutVariant"), + ) + val rules = ProguardRulesGenerator.generate(entries, modulePath) + assertContains(rules, "isDarkModeEnabled") + assertFalse(rules.contains("checkoutVariant"), "Enum flags must not produce -assumevalues rules") + } + + @Test + fun `returns blank when only enum flags are present`() { + val entries = listOf(entry("checkout_variant", "LEGACY", "com.example.CheckoutVariant")) + assertTrue( + ProguardRulesGenerator.generate(entries, modulePath).isBlank(), + "Expected blank rules when only enum flags are declared", + ) + } + // ── module-path-based class name ───────────────────────────────────────── @Test From e4a75951803a64fa1e93653bdb0ce25cd4ca9d38 Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sun, 17 May 2026 13:48:32 +0300 Subject: [PATCH 2/2] Validate enum typeFqn and document enum DSL - Require '.' in typeFqn so unqualified names fail at configuration time rather than as a generated-code compile error. - Add enum example to featured-gradle-plugin/CLAUDE.md DSL section. --- featured-gradle-plugin/CLAUDE.md | 1 + .../dev/androidbroadcast/featured/gradle/FlagContainer.kt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/featured-gradle-plugin/CLAUDE.md b/featured-gradle-plugin/CLAUDE.md index c3863eb..a1858b2 100644 --- a/featured-gradle-plugin/CLAUDE.md +++ b/featured-gradle-plugin/CLAUDE.md @@ -13,6 +13,7 @@ featured { localFlags { boolean("dark_mode", default = false) { category = "UI" } int("max_retries", default = 3) + enum("checkout_variant", typeFqn = "com.example.CheckoutVariant", default = "LEGACY") } remoteFlags { boolean("promo_banner", default = false) { description = "Show promo banner" } diff --git a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt index ea3595c..5003422 100644 --- a/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt +++ b/featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagContainer.kt @@ -98,6 +98,9 @@ public class FlagContainer { default: String, configure: FlagSpec.() -> Unit = {}, ) { + require('.' in typeFqn) { + "typeFqn must be a fully-qualified class name (e.g. \"com.example.MyEnum\"), got \"$typeFqn\"" + } _flags += FlagSpec(key = key, defaultValue = default, type = typeFqn).apply(configure) }