Skip to content

Add R8 -checkdiscard verification for disabled-flag dead code#295

Open
kirich1409 wants to merge 1 commit into
developfrom
claude/elegant-goldberg-r6ri7r
Open

Add R8 -checkdiscard verification for disabled-flag dead code#295
kirich1409 wants to merge 1 commit into
developfrom
claude/elegant-goldberg-r6ri7r

Conversation

@kirich1409

@kirich1409 kirich1409 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Why

The plugin already causes dead-code elimination with generated -assumevalues rules, but nothing verifies the disabled-flag code actually left the final binary. As noted in the discussion that prompted this: unit tests see the full pre-R8 classpath, androidTest is assembled without minify, so nothing checks the final DEX. R8's built-in -checkdiscard directive is exactly the missing piece — it lists feature classes and fails the build if they survive optimization (Discard checks failed + the retention path; diagnose with -whyareyoukeeping).

Paired with -assumevalues this closes the loop: the flag is pinned to false, its branch and classes are stripped, and -checkdiscard confirms their absence. A forgotten DI reference, manifest entry, reflection, or over-broad -keep that keeps a "disabled" feature alive is now caught at build time instead of shipping.

What

A new opt-in discard(...) on the flag DSL plus a dedicated task, wired into R8 exactly like the existing -assumevalues machinery.

featured {
    localFlags {
        boolean("new_checkout", default = false) {
            discard("com.example.checkout.newflow.**")
        }
    }
}

generates:

# guarded by flag: new_checkout
-checkdiscard class com.example.checkout.newflow.** { *; }

Surface

  • FlagSpec.discard(classSpec) — repeatable; rejected on non-boolean flags, on default = true flags, and on remote flags (a value never pinned at build time can't be guaranteed). Validation fails fast in the DSL with a precise message.
  • CheckDiscardRulesGenerator — pure, testable rule generator (header + one -checkdiscard line per target).
  • generateFeaturedCheckDiscardRules task → build/featured/proguard-featured-checkdiscard.pro. Reads discard targets straight from the DSL (config-cache safe, participates in the @Input fingerprint). The output is blank (a harmless no-op) unless a flag opts in.
  • Wiring mirrors -assumevalues: into the app's own R8 (proguardFiles) for application modules, and as consumer ProGuard rules for library / KMP-library modules so the check runs in the consuming app's R8 — wherever the matching -assumevalues rule applies.

Tests

  • CheckDiscardRulesGeneratorTest — output shape, trimming, blank-skipping, one rule per target.
  • FlagDiscardDslTest — recording, descriptor serialization, and all three validation rejections.
  • GenerateCheckDiscardRulesTaskRegistrationTest — task registration / group / wiring.
  • R8CheckDiscardTest (real R8, in featured-shrinker-tests) — proves the check passes when the flag-guarded class is eliminated and fails the build (CompilationFailedException) when a checked class survives.

Docs

Updated root CLAUDE.md and featured-gradle-plugin/CLAUDE.md (DSL example, task table, discard(...) semantics and wiring notes).

Notes / scope

  • Covers code only-checkdiscard does not verify resource shrinking (call out in docs; resources still need aapt2/apkanalyzer inspection).
  • No changes to the resolved-flags scan / manifest format — discard targets flow through their own provider, keeping blast radius small.
  • Existing -assumevalues generation and wiring are untouched; the empty check-discard file is the same already-handled case as an empty -assumevalues file for flag-less modules.

Targets develop per the project's branching convention.


Generated by Claude Code

Review in cubic

Local boolean flags (default = false) can now declare discard(...) class
specs. The plugin emits R8 -checkdiscard rules that *verify* the flag-guarded
code was actually stripped from the optimized release binary — the missing
verification half of the -assumevalues dead-code-elimination story. Unit tests
see the pre-R8 classpath and androidTest runs without minify, so without
-checkdiscard nothing checks the final DEX; a forgotten DI reference, manifest
entry, reflection, or over-broad -keep that keeps a "disabled" feature alive is
now caught at build time (Discard checks failed) instead of shipping.

- discard(classSpec) on FlagSpec; rejected on non-boolean, default=true, and
  remote flags (a value never pinned at build time can't be guaranteed).
- CheckDiscardRulesGenerator + generateFeaturedCheckDiscardRules task, wired
  into R8 like -assumevalues: app proguardFiles and library/KMP consumer rules.
- Real-R8 shrinker test proving the check passes when code is eliminated and
  fails the build when a checked class survives.
- Unit + registration + DSL-validation tests; docs in both CLAUDE.md files.

https://claude.ai/code/session_01HqnfagV6KswrzeqKLywpaS
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 13 files

Re-trigger cubic

@kirich1409 kirich1409 self-assigned this Jun 14, 2026
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.

2 participants