Skip to content

Add ForbiddenPublicSealedClass detekt rule#3503

Merged
tonidero merged 4 commits into
mainfrom
add-forbidden-public-sealed-class-detekt-rule
Jun 1, 2026
Merged

Add ForbiddenPublicSealedClass detekt rule#3503
tonidero merged 4 commits into
mainfrom
add-forbidden-public-sealed-class-detekt-rule

Conversation

@tonidero

@tonidero tonidero commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a new :detekt-rules Gradle module with a custom ForbiddenPublicSealedClass detekt rule
  • The rule flags public sealed class and public sealed interface declarations in non-example paths, unless annotated with @InternalRevenueCatAPI
  • Mirrors the existing ForbiddenPublicDataClass enforcement pattern from detekt-rules-libraries
  • Pre-existing violations (intentionally public sealed types already in the SDK) are added to the detekt baseline
  • The rule correctly ignores sealed classes nested inside internal classes (e.g. SubscriberAttributeKey subtypes)

Why: Adding a new subclass to a public sealed class/interface is a binary-incompatible change — any exhaustive when in consumer code breaks at runtime. Catching this at lint time prevents accidental API breakage.

Baselined pre-existing violations:

  • AmazonPurchasingData, GalaxyPurchasingData, GooglePurchasingData
  • RedeemWebPurchaseListener.Result
  • PaywallResult, PaywallFont, PurchaseLogicResult
  • Purchases.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-rules Gradle module and wires it into root detekt via detektPlugins(project(":detekt-rules")), alongside a new revenuecat rule set in config/detekt/detekt.yml.

The ForbiddenPublicSealedClass rule reports public sealed classes/interfaces (excluding examples/** and types annotated with @InternalRevenueCatAPI), including awareness of visibility when nested under non-public parents. Existing SDK violations are recorded in detekt-baseline.xml so CI stays green. Unit tests cover the rule; CircleCI runs :detekt-rules:test after prepare-tests.

Reviewed by Cursor Bugbot for commit 75068ee. Bugbot is set up for automated code reviews on this repo. Configure here.

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>
@socket-security

socket-security Bot commented May 22, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedmaven/​io.gitlab.arturbosch.detekt/​detekt-test@​1.23.89610090100100
Updatedmaven/​io.gitlab.arturbosch.detekt/​detekt-api@​1.23.6 ⏵ 1.23.893 +610090100100

View full report

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.89%. Comparing base (8d7b612) to head (75068ee).
⚠️ Report is 13 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tonidero tonidero marked this pull request as ready for review May 28, 2026 16:54
@tonidero tonidero requested a review from a team as a code owner May 28, 2026 16:54
ajpallares added a commit to RevenueCat/purchases-ios that referenced this pull request May 29, 2026
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 ajpallares left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for adding this, even though it hurts a bit 🥲

I added some comments!

Comment on lines -55 to -56
<ID>ModifierComposed:Placeholder.kt$placeholder</ID>
<ID>ModifierMissing:AppInfoScreen.kt$AppInfoScreen</ID>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Were these removed lines intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, those were removed automatically when updating the baseline, so looks like they are not a problem anymore :)

ajpallares added a commit to RevenueCat/purchases-ios that referenced this pull request May 29, 2026
…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>
Comment thread config/detekt/detekt.yml
Comment on lines +42 to +47
revenuecat:
active: true
ForbiddenPublicSealedClass:
active: true
excludes: [ '**/examples/**' ]
ignoreAnnotated: [ 'InternalRevenueCatAPI' ]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

❤️

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

💔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

💟

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🫶

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok. I lose (long live enums and sealed classes 🥹)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tonidero tonidero requested review from a team, JayShortway and ajpallares May 29, 2026 13:04

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread config/detekt/detekt-baseline.xml Outdated

@ajpallares ajpallares left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great job on this!

@tonidero tonidero added this pull request to the merge queue Jun 1, 2026
Merged via the queue into main with commit c3e9a45 Jun 1, 2026
40 checks passed
@tonidero tonidero deleted the add-forbidden-public-sealed-class-detekt-rule branch June 1, 2026 08:27
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
**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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants