Add ForbiddenPublicSealedClass detekt rule#3503
Conversation
Public sealed classes and interfaces in SDK public APIs are a binary compatibility hazard: adding a new subclass breaks any exhaustive `when` in consumer code. This adds a custom `:detekt-rules` module with a `ForbiddenPublicSealedClass` rule that mirrors the existing `ForbiddenPublicDataClass` enforcement, suppressed via `@InternalRevenueCatAPI`. Pre-existing violations are baselined. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3503 +/- ##
=======================================
Coverage 79.89% 79.89%
=======================================
Files 369 369
Lines 14871 14871
Branches 2048 2048
=======================================
Hits 11881 11881
Misses 2157 2157
Partials 833 833 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the `no_new_public_enums` custom SwiftLint rule that flags any new `public enum` declared in the SDK's consumer-facing surface, including `@_spi(Experimental)` APIs. Enums marked `@_spi(Internal)` on the same line as the declaration are exempt because they are not part of the consumer-facing surface. Existing public enums are grandfathered via `swiftlint-baseline.json`, which SwiftLint reads automatically through the `baseline:` key in `.swiftlint.yml`. Refactors that remove a baselined entry continue to pass; only NEW violations fail the build. New entries can be added by regenerating the baseline with `swiftlint lint --write-baseline swiftlint-baseline.json`, which requires an explicit, reviewable JSON change. This mirrors the approach taken on Android in RevenueCat/purchases-android#3503, which adds a custom detekt rule `ForbiddenPublicSealedClass` with the equivalent semantics for sealed classes/interfaces. Why: adding a case to an existing `public enum` is source-breaking for any consumer with an exhaustive `switch`. The policy avoids new public enums entirely; structs of static constants are the recommended alternative. `AGENTS.md` and the `Important Files` list are updated to document the new policy and the baseline file. Co-authored-by: Cursor <cursoragent@cursor.com>
ajpallares
left a comment
There was a problem hiding this comment.
Thank you for adding this, even though it hurts a bit 🥲
I added some comments!
| <ID>ModifierComposed:Placeholder.kt$placeholder</ID> | ||
| <ID>ModifierMissing:AppInfoScreen.kt$AppInfoScreen</ID> |
There was a problem hiding this comment.
Were these removed lines intentional?
There was a problem hiding this comment.
Yeah, those were removed automatically when updating the baseline, so looks like they are not a problem anymore :)
…6778) * CI: Enforce no-new-public-enums policy for consumer-facing APIs Per company policy, no new `enum` types may be added to consumer-facing APIs (fully public + `@_spi(Experimental)`). This adds CI-level enforcement so the policy stops being just a guideline. Adds a `check_public_enums` Fastlane lane that consumes the `.private.swiftinterface` files emitted alongside the public ones, extracts every consumer-facing `enum` declaration, and diffs against an allowlist baseline file per scheme. Internal SPI is intentionally excluded. Wired into both `check-api-changes-revenuecat` and `check-api-changes-revenuecatui` CI jobs. Allowlists seeded from the currently-committed swiftinterface files (45 entries for RevenueCat, 3 for RevenueCatUI). To intentionally add a new consumer-facing enum (with API-council approval), regenerate the allowlist: bundle exec fastlane ios check_public_enums scheme:RevenueCat regenerate:true Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "CI: Enforce no-new-public-enums policy for consumer-facing APIs" This reverts commit d671999. * Lint: Enforce no-new-public-enums policy via SwiftLint custom rule Adds the `no_new_public_enums` custom SwiftLint rule that flags any new `public enum` declared in the SDK's consumer-facing surface, including `@_spi(Experimental)` APIs. Enums marked `@_spi(Internal)` on the same line as the declaration are exempt because they are not part of the consumer-facing surface. Existing public enums are grandfathered via `swiftlint-baseline.json`, which SwiftLint reads automatically through the `baseline:` key in `.swiftlint.yml`. Refactors that remove a baselined entry continue to pass; only NEW violations fail the build. New entries can be added by regenerating the baseline with `swiftlint lint --write-baseline swiftlint-baseline.json`, which requires an explicit, reviewable JSON change. This mirrors the approach taken on Android in RevenueCat/purchases-android#3503, which adds a custom detekt rule `ForbiddenPublicSealedClass` with the equivalent semantics for sealed classes/interfaces. Why: adding a case to an existing `public enum` is source-breaking for any consumer with an exhaustive `switch`. The policy avoids new public enums entirely; structs of static constants are the recommended alternative. `AGENTS.md` and the `Important Files` list are updated to document the new policy and the baseline file. Co-authored-by: Cursor <cursoragent@cursor.com> * Lint: Simplify no_new_public_enums message Drops the `@_spi(Internal)` suggestion (not a real alternative when a public API is needed) and the baseline mention from the user-facing message. Regenerates `swiftlint-baseline.json` since SwiftLint baselines key off the violation message. Co-authored-by: Cursor <cursoragent@cursor.com> * Lint: Scope no_new_public_enums to SDK source paths Replaces the previous "lint everything except Tests/" approach with an explicit `included:` list of the actual SDK source roots: Sources/, RevenueCatUI/, and AdapterSDKs/RevenueCatAdMob/Sources/. Sample apps, backend integration tests, dev tooling, and similar non-SDK Swift files are no longer in scope for this rule. Coverage of the existing 56 grandfathered violations is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * Docs: Trim AGENTS.md public-enum section Co-authored-by: Cursor <cursoragent@cursor.com> * Docs: Drop baseline-justification note from AGENTS.md Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
| revenuecat: | ||
| active: true | ||
| ForbiddenPublicSealedClass: | ||
| active: true | ||
| excludes: [ '**/examples/**' ] | ||
| ignoreAnnotated: [ 'InternalRevenueCatAPI' ] |
There was a problem hiding this comment.
Ok. I lose (long live enums and sealed classes 🥹)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b02e4e2. Configure here.
**This is an automatic release.** ## RevenueCat SDK ### ✨ New Features * Add presented offering context to custom paywall events (RevenueCat#3424) via Rick (@rickvdl) * Add Workflows list endpoint (RevenueCat#3509) via Cesar de la Vega (@vegaro) ## RevenueCatUI SDK ### Paywalls_v2 #### 🐞 Bugfixes * Fix 1px seam between sliding multipage paywall pages (RevenueCat#3526) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * refactor: extract Offering.presentedOfferingContext() helper and apply across SDK (RevenueCat#3513) via Rick (@rickvdl) * Add JSON Logic string + array operators (RevenueCat#3485) via Antonio Pallares (@ajpallares) * Add ForbiddenPublicSealedClass detekt rule (RevenueCat#3503) via Toni Rico (@tonidero) * Update baseline profiles (RevenueCat#3519) via RevenueCat Git Bot (@RCGitBot) * build(deps): bump fastlane-plugin-revenuecat_internal from `af7bb5c` to `ce6a7ef` (RevenueCat#3515) via dependabot[bot] (@dependabot[bot]) * Add JSON Logic comparison operators (<, <=, >, >=) (RevenueCat#3484) via Antonio Pallares (@ajpallares) * Add JSON Logic arithmetic operators (+, -, *, /, %) (RevenueCat#3483) via Antonio Pallares (@ajpallares) * Add WorkflowEvent model and backend serialization (RevenueCat#3486) via Cesar de la Vega (@vegaro) * RulesEngine: add JSON Logic predicate evaluator (RevenueCat#3482) via Antonio Pallares (@ajpallares) * Add :rules-engine-internal skeleton module (RevenueCat#3478) via Antonio Pallares (@ajpallares) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Version bump and changelog/docs/CI path updates only; no application logic changes in the diff. > > **Overview** > This **automatic release** finalizes **Android SDK 10.8.0** by replacing **`10.8.0-SNAPSHOT`** with **`10.8.0`** across versioning (`gradle.properties`, `.version`, `Config.frameworkVersion`), sample apps, and changelog files. > > Release notes for **10.8.0** are recorded in **`CHANGELOG.md`** / **`CHANGELOG.latest.md`** (workflows list API, paywall offering context on custom events, multipage paywall seam fix, rules-engine/JSON Logic work, etc.). **Docs publishing** now targets **`10.8.0`** on S3, and **`docs/index.html`** redirects to the new doc URL. > > There are **no functional code changes** in this diff beyond version strings and release metadata. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c3048b8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->

Summary
:detekt-rulesGradle module with a customForbiddenPublicSealedClassdetekt rulepublic sealed classandpublic sealed interfacedeclarations in non-example paths, unless annotated with@InternalRevenueCatAPIForbiddenPublicDataClassenforcement pattern fromdetekt-rules-librariesinternalclasses (e.g.SubscriberAttributeKeysubtypes)Why: Adding a new subclass to a public sealed class/interface is a binary-incompatible change — any exhaustive
whenin consumer code breaks at runtime. Catching this at lint time prevents accidental API breakage.Baselined pre-existing violations:
AmazonPurchasingData,GalaxyPurchasingData,GooglePurchasingDataRedeemWebPurchaseListener.ResultPaywallResult,PaywallFont,PurchaseLogicResultPurchases.DeepLink(custom entitlement computation variant)Note
Low Risk
Static analysis and CI wiring only; no runtime SDK behavior changes beyond baselined lint findings.
Overview
Introduces a
:detekt-rulesGradle module and wires it into rootdetektviadetektPlugins(project(":detekt-rules")), alongside a newrevenuecatrule set inconfig/detekt/detekt.yml.The
ForbiddenPublicSealedClassrule reports public sealed classes/interfaces (excludingexamples/**and types annotated with@InternalRevenueCatAPI), including awareness of visibility when nested under non-public parents. Existing SDK violations are recorded indetekt-baseline.xmlso CI stays green. Unit tests cover the rule; CircleCI runs:detekt-rules:testafterprepare-tests.Reviewed by Cursor Bugbot for commit 75068ee. Bugbot is set up for automated code reviews on this repo. Configure here.