Skip to content

feat(gradle-plugin): generate ProGuard/R8 -assumevalues rules for Android/JVM#80

Merged
kirich1409 merged 2 commits into
mainfrom
feat/issue-16-feat-gradle-plugin-generate-proguard-r8--
Mar 21, 2026
Merged

feat(gradle-plugin): generate ProGuard/R8 -assumevalues rules for Android/JVM#80
kirich1409 merged 2 commits into
mainfrom
feat/issue-16-feat-gradle-plugin-generate-proguard-r8--

Conversation

@kirich1409
Copy link
Copy Markdown
Contributor

Summary

  • Adds ProguardRulesGenerator — pure logic object that generates a single -assumevalues block for ConfigValues, listing only @LocalFlag Boolean flags with defaultValue = false as comments, plus one shared isEnabled(String) return false member rule enabling R8 dead-branch elimination
  • Adds GenerateProguardRulesTask — Gradle task that reads ScanLocalFlagsTask's pipe-delimited output file and writes build/featured/proguard-featured.pro
  • Wires generateProguardRules task into FeaturedPlugin; it depends on scanLocalFlags and runs in the featured group
  • No rules are generated for @RemoteFlag, unannotated flags, defaultValue = true, or non-Boolean types
  • 14 new tests: 9 for generator logic, 5 for task registration and dependency wiring

Test plan

  • ./gradlew :featured-gradle-plugin:test — 35 tests, 0 failures
  • ./gradlew :core:koverVerify — core coverage ≥ 90%
  • ./gradlew spotlessCheck — formatting clean
  • ./gradlew test :core:koverVerify spotlessCheck — BUILD SUCCESSFUL

Closes #16

🤖 Generated with Claude Code

kirich1409 and others added 2 commits March 22, 2026 00:48
…calflag flags

- Add ProguardRulesGenerator object that emits -assumevalues blocks only
  for Boolean @localflag entries with defaultValue=false, enabling R8
  dead-branch elimination for disabled feature flags
- Add GenerateProguardRulesTask (reads ScanLocalFlagsTask output, writes
  build/featured/proguard-featured.pro)
- Wire generateProguardRules task into FeaturedPlugin; task depends on
  scanLocalFlags and runs within the 'featured' group
- 14 new tests covering generator logic and task registration

Closes #16

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 21, 2026 21:51
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Generate ProGuard/R8 -assumevalues rules for disabled @localflag flags

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add ProGuard/R8 rule generation for disabled feature flags
• Implement ProguardRulesGenerator to emit -assumevalues blocks
• Create GenerateProguardRulesTask Gradle task for rule file generation
• Wire task into plugin with dependency on scanLocalFlags task
• Add 14 comprehensive tests for generator and task registration
Diagram
flowchart LR
  A["ScanLocalFlagsTask<br/>output file"] -->|pipe-delimited<br/>flag entries| B["GenerateProguardRulesTask"]
  B -->|parse entries| C["ProguardRulesGenerator"]
  C -->|filter Boolean<br/>defaultValue=false| D["proguard-featured.pro<br/>-assumevalues block"]
  E["FeaturedPlugin"] -->|registers| B
  B -->|depends on| A
Loading

Grey Divider

File Changes

1. featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt ✨ Enhancement +20/-0

Wire ProGuard task registration into plugin

• Add GENERATE_PROGUARD_TASK_NAME constant for task naming
• Register GenerateProguardRulesTask in plugin's apply() method
• Implement registerProguardGenerationTask() to configure task with input/output files and
 dependencies
• Wire task to depend on scanLocalFlags task and assign to "featured" group

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt


2. featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt ✨ Enhancement +81/-0

Implement Gradle task for ProGuard rule generation

• Create new Gradle task class extending DefaultTask for ProGuard rule generation
• Define scanResultFile input property reading pipe-delimited flag entries
• Define outputFile output property for generated proguard-featured.pro file
• Implement generate() task action to parse entries, generate rules, and write output file
• Add logging for rule generation results and empty file handling

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt


3. featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt ✨ Enhancement +48/-0

Implement ProGuard rule generation logic

• Create pure logic object generating -assumevalues ProGuard rule blocks
• Filter entries to only Boolean flags with defaultValue = "false"
• Generate single shared isEnabled(String) return false method rule
• Return blank string when no qualifying entries exist
• Include flag keys and module names as comments in generated rules

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt


View more (2)
4. featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt 🧪 Tests +83/-0

Test ProGuard task registration and wiring

• Add 5 tests verifying task registration and configuration
• Test task existence, type, group assignment, and output file setup
• Verify task dependency on scanLocalFlags task
• Use ProjectBuilder for isolated Gradle plugin testing

featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt


5. featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt 🧪 Tests +117/-0

Test ProGuard rule generation logic

• Add 9 tests covering generator logic and edge cases
• Test empty entries, non-boolean flags, and defaultValue = true filtering
• Verify correct -assumevalues format and ConfigValues class reference
• Test multiple flag handling and return false method rule inclusion
• Validate ProGuard rule format compliance

featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Mar 21, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (4) 📐 Spec deviations (0)

Grey Divider


Action required

1. generateProguardRules not release-wired 📎 Requirement gap ✓ Correctness
Description
The plugin registers generateProguardRules but does not wire it into any Android release/minify
task, so the rules file is not generated as part of release builds. This fails the requirement that
rule generation happens during the release build flow.
Code

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt[R20-26]

public class FeaturedPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        val scanTask = registerModuleScanTask(target)
+        registerProguardGenerationTask(target, scanTask)
        wireModuleTaskToRootAggregator(target, scanTask)
    }
Evidence
PR Compliance ID 1 requires generating the ProGuard/R8 rules file as part of release builds. The
plugin only registers generateProguardRules and sets inputs/outputs, but there is no wiring to
assembleRelease/minifyReleaseWithR8 (or similar) to ensure it runs during release builds.

Generate ProGuard/R8 -assumevalues rules for @LocalFlag ConfigParams with defaultValue=false in release builds
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt[20-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`generateProguardRules` is registered but not executed as part of release builds, so the rules file may not exist when R8 runs.

## Issue Context
Compliance requires the rules file to be generated during the release build flow.

## Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt[20-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. isEnabled() assumevalues disables all 📎 Requirement gap ✓ Correctness
Description
The generated -assumevalues rule forces ConfigValues.isEnabled(String) to always return false,
which is not limited to only @LocalFlag defaultValue=false flags and would effectively freeze all
flags to disabled. This violates the requirement to apply assumevalues only to qualifying
@LocalFlag defaultValue=false params.
Code

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[R19-45]

+    /**
+     * ProGuard member rule that tells R8 `isEnabled(String)` always returns `false`.
+     * A single copy covers all disabled flags — R8 applies it per call-site key.
+     *
+     * Update this constant when the `isEnabled` method signature is finalised.
+     */
+    private const val IS_ENABLED_RULE = "boolean isEnabled(java.lang.String) return false;"
+
+    /**
+     * Generates a ProGuard rules string from [entries].
+     *
+     * Returns a blank string when no qualifying entries exist (Boolean type,
+     * `defaultValue == "false"`). Otherwise returns a single `-assumevalues`
+     * block listing the disabled flag keys as comments and one shared method rule.
+     */
+    public fun generate(entries: List<LocalFlagEntry>): String {
+        val disabled = entries.filter { it.type == "Boolean" && it.defaultValue == "false" }
+        if (disabled.isEmpty()) return ""
+
+        return buildString {
+            appendLine("-assumevalues class $CONFIG_VALUES_CLASS {")
+            appendLine("    # Flags frozen at release build time — default value is false")
+            disabled.forEach { entry ->
+                appendLine("    # key: ${entry.key} (module: ${entry.moduleName})")
+            }
+            appendLine("    $IS_ENABLED_RULE")
+            append("}")
Evidence
PR Compliance ID 1 requires assumevalues rules for all and only @LocalFlag defaultValue=false
params. The generator emits a single unconditional member rule `boolean isEnabled(java.lang.String)
return false;` and only lists keys as comments, so the produced ProGuard rule is not scoped to
specific qualifying flags.

Generate ProGuard/R8 -assumevalues rules for @LocalFlag ConfigParams with defaultValue=false in release builds
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[19-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The generator emits an unconditional assumevalues rule for `isEnabled(String)` returning `false`, which is not restricted to only `@LocalFlag` entries with `defaultValue=false`.

## Issue Context
Compliance requires the generated rules to apply to all and only qualifying `@LocalFlag defaultValue=false` flags.

## Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[19-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. proguard-featured.pro requires manual setup 📎 Requirement gap ✓ Correctness
Description
The implementation does not automatically add the generated rules file to the Android module’s
ProGuard/R8 configuration, and instead documents a manual proguardFiles(...) step. This violates
the requirement that the rules file be applied automatically during release minification.
Code

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt[R19-29]

+ * Wire the generated file into your Android module's ProGuard configuration:
+ * ```kotlin
+ * android {
+ *     buildTypes {
+ *         release {
+ *             proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),
+ *                           "proguard-featured.pro")
+ *         }
+ *     }
+ * }
+ * ```
Evidence
PR Compliance ID 2 requires automatically wiring the generated rules file into the Android module’s
ProGuard/R8 configuration. The task documentation instructs developers to manually add
proguardFiles(...), and there is no plugin code shown that configures Android build types/variants
to consume the generated file automatically.

Automatically add the generated rules file to the Android module ProGuard/R8 configuration
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt[19-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The generated ProGuard rules file is not automatically applied to Android release minification; developers are instructed to manually configure `proguardFiles`.

## Issue Context
Compliance requires zero manual steps: the Android module must consume the generated rules during release builds.

## Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt[20-56]
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt[19-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Missing R8 integration test 📎 Requirement gap ⛯ Reliability
Description
The PR adds only unit tests for rule generation and task registration, but no end-to-end integration
test that builds a minified/R8 variant and asserts dead code elimination from the generated rules.
This fails the requirements to verify R8 elimination behavior and to provide an automated
integration test covering generation + application + elimination.
Code

featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt[R9-56]

+class ProguardRulesGeneratorTest {
+    @Test
+    fun `generates no rules when entries list is empty`() {
+        val rules = ProguardRulesGenerator.generate(emptyList())
+        assertTrue(
+            rules.isBlank(),
+            "Expected blank output for empty entries, got: '$rules'",
+        )
+    }
+
+    @Test
+    fun `generates no rules for boolean flag with defaultValue true`() {
+        val entries =
+            listOf(
+                LocalFlagEntry(key = "feature_enabled", defaultValue = "true", type = "Boolean", moduleName = ":app"),
+            )
+        val rules = ProguardRulesGenerator.generate(entries)
+        assertTrue(
+            rules.isBlank(),
+            "Expected no rules for flags with defaultValue=true, got: '$rules'",
+        )
+    }
+
+    @Test
+    fun `generates no rules for non-boolean flags`() {
+        val entries =
+            listOf(
+                LocalFlagEntry(key = "timeout", defaultValue = "30", type = "Int", moduleName = ":app"),
+                LocalFlagEntry(key = "server_url", defaultValue = "https://example.com", type = "String", moduleName = ":app"),
+            )
+        val rules = ProguardRulesGenerator.generate(entries)
+        assertTrue(
+            rules.isBlank(),
+            "Expected no rules for non-boolean flags, got: '$rules'",
+        )
+    }
+
+    @Test
+    fun `generates assumevalues rule for boolean flag with defaultValue false`() {
+        val entries =
+            listOf(
+                LocalFlagEntry(key = "dark_mode", defaultValue = "false", type = "Boolean", moduleName = ":app"),
+            )
+        val rules = ProguardRulesGenerator.generate(entries)
+        assertContains(rules, "-assumevalues")
+        assertContains(rules, "dark_mode")
+        assertFalse(rules.isBlank())
+    }
Evidence
PR Compliance IDs 3 and 5 require demonstrating (via automated integration testing) that R8 consumes
the generated rules and eliminates guarded dead branches. The added tests only validate string
output and Gradle task registration, and do not run an R8/minified build or assert code removal.

Verify R8 eliminates dead code guarded by @LocalFlag defaultValue=false using generated assumevalues rules
Integration test exists to verify dead code elimination behavior
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt[9-56]
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt[8-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
There is no automated integration test that executes a minified/R8 build and asserts that code guarded by a qualifying `@LocalFlag defaultValue=false` is eliminated due to the generated rules.

## Issue Context
Compliance requires verifying the full pipeline: rules generation + rules application in release minification + observable dead code elimination.

## Fix Focus Areas
- featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt[9-56]
- featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt[8-73]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Missing isEnabled target 🐞 Bug ✓ Correctness
Description
ProguardRulesGenerator emits an -assumevalues member rule for
dev.androidbroadcast.featured.ConfigValues.isEnabled(String), but ConfigValues currently has no
such method, so including the generated file can produce R8/ProGuard warnings or shrink-step
failures for unknown members.
Code

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[R17-26]

+    private const val CONFIG_VALUES_CLASS = "dev.androidbroadcast.featured.ConfigValues"
+
+    /**
+     * ProGuard member rule that tells R8 `isEnabled(String)` always returns `false`.
+     * A single copy covers all disabled flags — R8 applies it per call-site key.
+     *
+     * Update this constant when the `isEnabled` method signature is finalised.
+     */
+    private const val IS_ENABLED_RULE = "boolean isEnabled(java.lang.String) return false;"
+
Evidence
The generator hardcodes a rule for isEnabled(String), but the current ConfigValues
implementation only exposes getValue, override, fetch, and observe—no isEnabled method
exists to match the emitted member rule.

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[17-26]
core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValues.kt[17-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProguardRulesGenerator` generates an `-assumevalues` rule for `ConfigValues.isEnabled(String)`, but `ConfigValues` currently does not define `isEnabled`, so the generated ProGuard file refers to an unknown member.

### Issue Context
This can surface as R8/ProGuard warnings or failures when consumers include `build/featured/proguard-featured.pro` in their shrink configuration.

### Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[17-26]
- core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValues.kt[17-65]

### What to change
- Either implement the exact `isEnabled(java.lang.String): boolean` API on `ConfigValues` (JVM/Android-visible signature), **or** stop emitting this member rule until the API exists.
- If the API will differ, make the signature configurable (extension/property) so the generator doesn’t hardcode a potentially invalid rule.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Nondeterministic rules output 🐞 Bug ⛯ Reliability
Description
The generated proguard-featured.pro preserves the scan/parse iteration order rather than sorting
by key/module, so output ordering can vary across runs and cause unnecessary rebuilds or cache
misses even when the set of flags is unchanged.
Code

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[R35-43]

+        val disabled = entries.filter { it.type == "Boolean" && it.defaultValue == "false" }
+        if (disabled.isEmpty()) return ""
+
+        return buildString {
+            appendLine("-assumevalues class $CONFIG_VALUES_CLASS {")
+            appendLine("    # Flags frozen at release build time — default value is false")
+            disabled.forEach { entry ->
+                appendLine("    # key: ${entry.key} (module: ${entry.moduleName})")
+            }
Evidence
ScanLocalFlagsTask writes entries in whatever order sourceFiles.flatMap { ... } produces, and
ProguardRulesGenerator iterates disabled in that same order when writing comment lines. Without
sorting, rule file content can change ordering without any semantic changes to inputs.

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanLocalFlagsTask.kt[64-77]
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[35-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Rule generation output order is not stabilized (no sorting), so the generated file may change ordering across runs.

### Issue Context
Deterministic task outputs improve Gradle build caching and reduce noisy diffs/rebuilds.

### Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[35-43]
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanLocalFlagsTask.kt[64-77]

### What to change
- Sort `disabled` entries before writing (e.g., by `moduleName` then `key`).
- Optionally also sort `sourceFiles`/entries at scan time to make both `local-flags.txt` and `proguard-featured.pro` stable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds ProGuard/R8 rules generation support to the featured-gradle-plugin, deriving output from existing @LocalFlag source scanning so Android/JVM builds can (eventually) benefit from R8 dead-branch elimination.

Changes:

  • Introduces ProguardRulesGenerator to produce a -assumevalues rules block from scanned @LocalFlag entries.
  • Adds GenerateProguardRulesTask to read the scan report and write build/featured/proguard-featured.pro.
  • Wires generateProguardRules into FeaturedPlugin and adds unit tests for generator logic and task registration/dependencies.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt New generator for ProGuard/R8 -assumevalues rules based on scanned local flags.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt New Gradle task to parse scan output and write the generated ProGuard rules file.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt Registers the new generateProguardRules task and wires it to depend on scanning.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt Adds generator-focused unit tests.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt Adds plugin/task registration and dependency wiring tests.

Comment on lines +4 to +46
* Generates ProGuard/R8 `-assumevalues` rules for `@LocalFlag`-annotated
* `ConfigParam` properties whose `defaultValue` is `false`.
*
* Rules are emitted only for Boolean flags frozen to `false` at release build
* time, enabling R8 dead-branch elimination for code guarded by those flags.
* Flags with `defaultValue = true`, non-boolean flags, and `@RemoteFlag`
* declarations are intentionally excluded.
*
* **Method signature note:** The generated rule targets `isEnabled(String)`
* on `ConfigValues`. This signature will be finalised when the `isEnabled`
* API lands; update [IS_ENABLED_RULE] if the signature changes.
*/
public object ProguardRulesGenerator {
private const val CONFIG_VALUES_CLASS = "dev.androidbroadcast.featured.ConfigValues"

/**
* ProGuard member rule that tells R8 `isEnabled(String)` always returns `false`.
* A single copy covers all disabled flags — R8 applies it per call-site key.
*
* Update this constant when the `isEnabled` method signature is finalised.
*/
private const val IS_ENABLED_RULE = "boolean isEnabled(java.lang.String) return false;"

/**
* Generates a ProGuard rules string from [entries].
*
* Returns a blank string when no qualifying entries exist (Boolean type,
* `defaultValue == "false"`). Otherwise returns a single `-assumevalues`
* block listing the disabled flag keys as comments and one shared method rule.
*/
public fun generate(entries: List<LocalFlagEntry>): String {
val disabled = entries.filter { it.type == "Boolean" && it.defaultValue == "false" }
if (disabled.isEmpty()) return ""

return buildString {
appendLine("-assumevalues class $CONFIG_VALUES_CLASS {")
appendLine(" # Flags frozen at release build time — default value is false")
disabled.forEach { entry ->
appendLine(" # key: ${entry.key} (module: ${entry.moduleName})")
}
appendLine(" $IS_ENABLED_RULE")
append("}")
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IS_ENABLED_RULE tells R8/ProGuard that ConfigValues.isEnabled(String) always returns false, which would make all feature checks constant-false (including keys with defaultValue=true and any remote flags) and can change app behavior. Also, there is currently no isEnabled(...) method on dev.androidbroadcast.featured.ConfigValues in this repo, so including this rule is likely to produce R8/ProGuard missing-member warnings or build failures when the generated file is consumed. Consider removing the method rule entirely until the API exists and can be targeted safely (e.g., via per-flag methods/fields rather than a key-based String API), or gate rule generation on the presence of that API.

Suggested change
* Generates ProGuard/R8 `-assumevalues` rules for `@LocalFlag`-annotated
* `ConfigParam` properties whose `defaultValue` is `false`.
*
* Rules are emitted only for Boolean flags frozen to `false` at release build
* time, enabling R8 dead-branch elimination for code guarded by those flags.
* Flags with `defaultValue = true`, non-boolean flags, and `@RemoteFlag`
* declarations are intentionally excluded.
*
* **Method signature note:** The generated rule targets `isEnabled(String)`
* on `ConfigValues`. This signature will be finalised when the `isEnabled`
* API lands; update [IS_ENABLED_RULE] if the signature changes.
*/
public object ProguardRulesGenerator {
private const val CONFIG_VALUES_CLASS = "dev.androidbroadcast.featured.ConfigValues"
/**
* ProGuard member rule that tells R8 `isEnabled(String)` always returns `false`.
* A single copy covers all disabled flags — R8 applies it per call-site key.
*
* Update this constant when the `isEnabled` method signature is finalised.
*/
private const val IS_ENABLED_RULE = "boolean isEnabled(java.lang.String) return false;"
/**
* Generates a ProGuard rules string from [entries].
*
* Returns a blank string when no qualifying entries exist (Boolean type,
* `defaultValue == "false"`). Otherwise returns a single `-assumevalues`
* block listing the disabled flag keys as comments and one shared method rule.
*/
public fun generate(entries: List<LocalFlagEntry>): String {
val disabled = entries.filter { it.type == "Boolean" && it.defaultValue == "false" }
if (disabled.isEmpty()) return ""
return buildString {
appendLine("-assumevalues class $CONFIG_VALUES_CLASS {")
appendLine(" # Flags frozen at release build time — default value is false")
disabled.forEach { entry ->
appendLine(" # key: ${entry.key} (module: ${entry.moduleName})")
}
appendLine(" $IS_ENABLED_RULE")
append("}")
}
* Placeholder for generating ProGuard/R8 rules for `@LocalFlag`-annotated
* `ConfigParam` properties.
*
* The generation of `-assumevalues` rules for `ConfigValues.isEnabled(...)`
* is intentionally disabled for now because there is no stable
* `isEnabled` API to target safely. Emitting such a rule prematurely could
* change runtime behaviour (for example, by forcing all flags to `false`)
* or cause R8/ProGuard to report missing-member warnings.
*
* When the `isEnabled` API is available and its signature is finalised,
* this generator can be updated to emit precise, per-flag rules.
*/
public object ProguardRulesGenerator {
/**
* Generates a ProGuard rules string from [entries].
*
* Currently returns a blank string because there is no stable
* `ConfigValues.isEnabled(...)` API to target safely.
*/
public fun generate(entries: List<LocalFlagEntry>): String {
// Intentionally no-op until the isEnabled API exists and can be
// targeted without changing runtime behaviour unexpectedly.
return ""

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +79
return file
.readLines()
.filter { it.isNotBlank() }
.mapNotNull { line ->
val parts = line.split("|")
if (parts.size != 4) return@mapNotNull null
LocalFlagEntry(
key = parts[0],
defaultValue = parts[1],
type = parts[2],
moduleName = parts[3],
)
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseScanResult() silently drops any line that doesn't split into exactly 4 parts. Because the format is a raw pipe-delimited string, a key/defaultValue/moduleName containing | (or any accidental extra delimiter) will cause the entry to be ignored without any visibility, potentially skipping rule generation. Consider switching the scan output to a robust encoding (e.g., JSON Lines) or at minimum logging a warning/failing the task when malformed lines are encountered so regressions aren’t hidden.

Suggested change
return file
.readLines()
.filter { it.isNotBlank() }
.mapNotNull { line ->
val parts = line.split("|")
if (parts.size != 4) return@mapNotNull null
LocalFlagEntry(
key = parts[0],
defaultValue = parts[1],
type = parts[2],
moduleName = parts[3],
)
}
val entries = mutableListOf<LocalFlagEntry>()
var malformedCount = 0
file.readLines().forEachIndexed { index, rawLine ->
val line = rawLine.trim()
if (line.isEmpty()) {
return@forEachIndexed
}
// Use a bounded split so that any additional '|' characters after the third
// delimiter are included in the last field instead of creating extra parts.
val parts = line.split("|", limit = 4)
if (parts.size != 4) {
malformedCount++
logger.warn(
"[featured] Malformed scan result line ${index + 1} in ${file.path}: " +
"expected 4 '|' separated fields but found ${parts.size}. " +
"Line content: `$line`"
)
return@forEachIndexed
}
entries += LocalFlagEntry(
key = parts[0],
defaultValue = parts[1],
type = parts[2],
moduleName = parts[3],
)
}
if (malformedCount > 0) {
logger.warn(
"[featured] Skipped $malformedCount malformed line(s) in scan result file ${file.path}; " +
"generated ProGuard rules may be incomplete."
)
}
return entries

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +56
private fun registerProguardGenerationTask(
target: Project,
scanTask: TaskProvider<ScanLocalFlagsTask>,
) {
target.tasks.register(GENERATE_PROGUARD_TASK_NAME, GenerateProguardRulesTask::class.java) { task ->
task.group = "featured"
task.description =
"Generates ProGuard/R8 -assumevalues rules for @LocalFlag(defaultValue=false) flags in '${target.path}'."
task.scanResultFile.set(scanTask.flatMap { it.outputFile })
task.outputFile.set(
target.layout.buildDirectory.file("featured/proguard-featured.pro"),
)
task.dependsOn(scanTask)
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description indicates this change “Closes #16”, but the implementation here only generates the ProGuard file via a task; it does not auto-wire the generated proguard-featured.pro into an Android module’s proguardFiles nor add an integration test validating R8 dead-code elimination (both called out in #16 acceptance criteria). Consider updating the PR/issue linkage (don’t close #16 yet) or extending the plugin to apply the generated file automatically for Android release builds and adding an integration test.

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 26
public class FeaturedPlugin : Plugin<Project> {
override fun apply(target: Project) {
val scanTask = registerModuleScanTask(target)
registerProguardGenerationTask(target, scanTask)
wireModuleTaskToRootAggregator(target, scanTask)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. generateproguardrules not release-wired 📎 Requirement gap ✓ Correctness

The plugin registers generateProguardRules but does not wire it into any Android release/minify
task, so the rules file is not generated as part of release builds. This fails the requirement that
rule generation happens during the release build flow.
Agent Prompt
## Issue description
`generateProguardRules` is registered but not executed as part of release builds, so the rules file may not exist when R8 runs.

## Issue Context
Compliance requires the rules file to be generated during the release build flow.

## Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt[20-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +19 to +45
/**
* ProGuard member rule that tells R8 `isEnabled(String)` always returns `false`.
* A single copy covers all disabled flags — R8 applies it per call-site key.
*
* Update this constant when the `isEnabled` method signature is finalised.
*/
private const val IS_ENABLED_RULE = "boolean isEnabled(java.lang.String) return false;"

/**
* Generates a ProGuard rules string from [entries].
*
* Returns a blank string when no qualifying entries exist (Boolean type,
* `defaultValue == "false"`). Otherwise returns a single `-assumevalues`
* block listing the disabled flag keys as comments and one shared method rule.
*/
public fun generate(entries: List<LocalFlagEntry>): String {
val disabled = entries.filter { it.type == "Boolean" && it.defaultValue == "false" }
if (disabled.isEmpty()) return ""

return buildString {
appendLine("-assumevalues class $CONFIG_VALUES_CLASS {")
appendLine(" # Flags frozen at release build time — default value is false")
disabled.forEach { entry ->
appendLine(" # key: ${entry.key} (module: ${entry.moduleName})")
}
appendLine(" $IS_ENABLED_RULE")
append("}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. isenabled() assumevalues disables all 📎 Requirement gap ✓ Correctness

The generated -assumevalues rule forces ConfigValues.isEnabled(String) to always return false,
which is not limited to only @LocalFlag defaultValue=false flags and would effectively freeze all
flags to disabled. This violates the requirement to apply assumevalues only to qualifying
@LocalFlag defaultValue=false params.
Agent Prompt
## Issue description
The generator emits an unconditional assumevalues rule for `isEnabled(String)` returning `false`, which is not restricted to only `@LocalFlag` entries with `defaultValue=false`.

## Issue Context
Compliance requires the generated rules to apply to all and only qualifying `@LocalFlag defaultValue=false` flags.

## Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[19-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +19 to +29
* Wire the generated file into your Android module's ProGuard configuration:
* ```kotlin
* android {
* buildTypes {
* release {
* proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),
* "proguard-featured.pro")
* }
* }
* }
* ```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. proguard-featured.pro requires manual setup 📎 Requirement gap ✓ Correctness

The implementation does not automatically add the generated rules file to the Android module’s
ProGuard/R8 configuration, and instead documents a manual proguardFiles(...) step. This violates
the requirement that the rules file be applied automatically during release minification.
Agent Prompt
## Issue description
The generated ProGuard rules file is not automatically applied to Android release minification; developers are instructed to manually configure `proguardFiles`.

## Issue Context
Compliance requires zero manual steps: the Android module must consume the generated rules during release builds.

## Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt[20-56]
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTask.kt[19-29]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +9 to +56
class ProguardRulesGeneratorTest {
@Test
fun `generates no rules when entries list is empty`() {
val rules = ProguardRulesGenerator.generate(emptyList())
assertTrue(
rules.isBlank(),
"Expected blank output for empty entries, got: '$rules'",
)
}

@Test
fun `generates no rules for boolean flag with defaultValue true`() {
val entries =
listOf(
LocalFlagEntry(key = "feature_enabled", defaultValue = "true", type = "Boolean", moduleName = ":app"),
)
val rules = ProguardRulesGenerator.generate(entries)
assertTrue(
rules.isBlank(),
"Expected no rules for flags with defaultValue=true, got: '$rules'",
)
}

@Test
fun `generates no rules for non-boolean flags`() {
val entries =
listOf(
LocalFlagEntry(key = "timeout", defaultValue = "30", type = "Int", moduleName = ":app"),
LocalFlagEntry(key = "server_url", defaultValue = "https://example.com", type = "String", moduleName = ":app"),
)
val rules = ProguardRulesGenerator.generate(entries)
assertTrue(
rules.isBlank(),
"Expected no rules for non-boolean flags, got: '$rules'",
)
}

@Test
fun `generates assumevalues rule for boolean flag with defaultValue false`() {
val entries =
listOf(
LocalFlagEntry(key = "dark_mode", defaultValue = "false", type = "Boolean", moduleName = ":app"),
)
val rules = ProguardRulesGenerator.generate(entries)
assertContains(rules, "-assumevalues")
assertContains(rules, "dark_mode")
assertFalse(rules.isBlank())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Missing r8 integration test 📎 Requirement gap ⛯ Reliability

The PR adds only unit tests for rule generation and task registration, but no end-to-end integration
test that builds a minified/R8 variant and asserts dead code elimination from the generated rules.
This fails the requirements to verify R8 elimination behavior and to provide an automated
integration test covering generation + application + elimination.
Agent Prompt
## Issue description
There is no automated integration test that executes a minified/R8 build and asserts that code guarded by a qualifying `@LocalFlag defaultValue=false` is eliminated due to the generated rules.

## Issue Context
Compliance requires verifying the full pipeline: rules generation + rules application in release minification + observable dead code elimination.

## Fix Focus Areas
- featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGeneratorTest.kt[9-56]
- featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateProguardRulesTaskRegistrationTest.kt[8-73]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +17 to +26
private const val CONFIG_VALUES_CLASS = "dev.androidbroadcast.featured.ConfigValues"

/**
* ProGuard member rule that tells R8 `isEnabled(String)` always returns `false`.
* A single copy covers all disabled flags — R8 applies it per call-site key.
*
* Update this constant when the `isEnabled` method signature is finalised.
*/
private const val IS_ENABLED_RULE = "boolean isEnabled(java.lang.String) return false;"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Missing isenabled target 🐞 Bug ✓ Correctness

ProguardRulesGenerator emits an -assumevalues member rule for
dev.androidbroadcast.featured.ConfigValues.isEnabled(String), but ConfigValues currently has no
such method, so including the generated file can produce R8/ProGuard warnings or shrink-step
failures for unknown members.
Agent Prompt
### Issue description
`ProguardRulesGenerator` generates an `-assumevalues` rule for `ConfigValues.isEnabled(String)`, but `ConfigValues` currently does not define `isEnabled`, so the generated ProGuard file refers to an unknown member.

### Issue Context
This can surface as R8/ProGuard warnings or failures when consumers include `build/featured/proguard-featured.pro` in their shrink configuration.

### Fix Focus Areas
- featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ProguardRulesGenerator.kt[17-26]
- core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValues.kt[17-65]

### What to change
- Either implement the exact `isEnabled(java.lang.String): boolean` API on `ConfigValues` (JVM/Android-visible signature), **or** stop emitting this member rule until the API exists.
- If the API will differ, make the signature configurable (extension/property) so the generator doesn’t hardcode a potentially invalid rule.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@kirich1409 kirich1409 merged commit dbbbc80 into main Mar 21, 2026
10 of 11 checks passed
@kirich1409 kirich1409 deleted the feat/issue-16-feat-gradle-plugin-generate-proguard-r8-- branch April 3, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gradle-plugin): generate ProGuard/R8 -assumevalues rules for Android/JVM

2 participants