Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions core/api/jvm/core.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
public abstract interface annotation class dev/androidbroadcast/featured/AssumesFlag : java/lang/annotation/Annotation {
public abstract fun flagName ()Ljava/lang/String;
}

public abstract interface annotation class dev/androidbroadcast/featured/BehindFlag : java/lang/annotation/Annotation {
public abstract fun flagName ()Ljava/lang/String;
}

public final class dev/androidbroadcast/featured/ConfigParam {
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Lkotlin/reflect/KClass;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Lkotlin/reflect/KClass;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.androidbroadcast.featured

/**
* Marks a function or class that takes explicit responsibility for ensuring the named feature
* flag is checked before execution reaches this scope.
*
* ## Purpose
*
* When a function or class always runs within a guarded context but cannot express that guard
* directly in its own body (e.g., a navigation host that conditionally renders flag-gated
* screens), annotate it with `@AssumesFlag` to suppress `UncheckedFlagAccess` warnings for
* call sites of `@BehindFlag`-annotated code inside this scope.
*
* ## Scope
*
* - On a **function**: suppresses warnings inside the function body.
* - On a **class**: suppresses warnings inside member functions and `init` blocks.
* Companion object members are **not** covered — they are a separate scope.
*
* ## ⚠️ Escape hatch
*
* This annotation is **not verified**. The Detekt rule trusts the annotation without
* checking that an actual flag guard exists inside the scope. Misuse silently bypasses
* `UncheckedFlagAccess`. Use it only when the calling context genuinely guarantees the
* flag is checked.
*
* ## Usage
*
* ```kotlin
* @AssumesFlag("newCheckout")
* fun CheckoutNavHost(configValues: ConfigValues) {
* // This function is only called when newCheckout is enabled upstream.
* NewCheckoutScreen() // no UncheckedFlagAccess warning here
* }
* ```
*
* This annotation has [AnnotationRetention.SOURCE] retention — zero runtime overhead.
*
* @see BehindFlag
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
public annotation class AssumesFlag(
/**
* The name of the feature flag property this scope guarantees is checked before execution.
* Must match the `flagName` of the corresponding `@BehindFlag` declaration.
*/
val flagName: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dev.androidbroadcast.featured

/**
* Marks a function, class, or property that must only be used inside a valid feature-flag
* context — i.e., where the named flag has been checked before execution.
*
* ## Intended workflow
*
* 1. **Declare the flag** — introduce a `ConfigParam` annotated with `@LocalFlag` or
* `@RemoteFlag`. The `flagName` here must match the exact Kotlin property name.
* 2. **Annotate guarded code** — place `@BehindFlag("flagName")` on every function, class,
* or property that must only run when the flag is active.
* 3. **Guard call sites** — wrap every call site in an `if`/`when` that checks the flag,
* or annotate the containing function/class with `@AssumesFlag("flagName")`.
* 4. **Let Detekt enforce it** — the `UncheckedFlagAccess` rule (requires
* `detektWithTypeResolution`) reports any call site that lacks a valid guard.
*
* ## Usage
*
* ```kotlin
* @LocalFlag
* val newCheckout = ConfigParam<Boolean>("new_checkout", defaultValue = false)
*
* @BehindFlag("newCheckout")
* fun NewCheckoutScreen() { ... }
*
* // Call site must be guarded:
* if (configValues[newCheckout]) {
* NewCheckoutScreen()
* }
* ```
*
* This annotation has [AnnotationRetention.SOURCE] retention — zero runtime overhead.
*
* @see AssumesFlag
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
public annotation class BehindFlag(
/**
* The name of the Kotlin property (declared with `@LocalFlag` or `@RemoteFlag`)
* that guards this declaration. Must match the exact property name, e.g. `"newCheckout"`.
*
* Validated by the `InvalidFlagReference` Detekt rule within the same file.
*/
val flagName: String,
)
Loading
Loading