feat(detekt): add @BehindFlag/@AssumesFlag annotations and InvalidFlagReference/UncheckedFlagAccess rules#142
Conversation
There was a problem hiding this comment.
Pull request overview
Adds new core annotations and Detekt rules to statically enforce feature-flag guarding, plus design/plan docs for the change.
Changes:
- Introduce
@BehindFlag/@AssumesFlag(SOURCE retention) annotations in:corefor declaring flag-guard expectations. - Add two Detekt rules in
:featured-detekt-rules:InvalidFlagReference(PSI-only) andUncheckedFlagAccess(type-resolution-based). - Register the new rules in
FeaturedRuleSetProvider, add API dumps, and add unit tests + design/implementation docs.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/commonMain/kotlin/dev/androidbroadcast/featured/BehindFlag.kt | Adds @BehindFlag annotation for marking guarded declarations. |
| core/src/commonMain/kotlin/dev/androidbroadcast/featured/AssumesFlag.kt | Adds @AssumesFlag annotation for “guard is handled upstream” scopes. |
| core/api/jvm/core.api | Updates JVM API surface to include the new annotations. |
| featured-detekt-rules/src/main/kotlin/dev/androidbroadcast/featured/detekt/InvalidFlagReference.kt | Adds PSI-only validation of @BehindFlag/@AssumesFlag string references within a file. |
| featured-detekt-rules/src/main/kotlin/dev/androidbroadcast/featured/detekt/UncheckedFlagAccess.kt | Adds type-resolution rule to detect unguarded access to @BehindFlag declarations. |
| featured-detekt-rules/src/main/kotlin/dev/androidbroadcast/featured/detekt/FeaturedRuleSetProvider.kt | Registers the new rules and updates rule-set documentation. |
| featured-detekt-rules/src/test/kotlin/dev/androidbroadcast/featured/detekt/InvalidFlagReferenceTest.kt | Adds tests for unknown/typo flag references and ordering behavior. |
| featured-detekt-rules/src/test/kotlin/dev/androidbroadcast/featured/detekt/UncheckedFlagAccessTest.kt | Adds type-resolution tests for valid contexts and violation cases. |
| featured-detekt-rules/api/featured-detekt-rules.api | Updates Detekt rules module API dump with the new rule classes. |
| docs/superpowers/specs/2026-03-23-behind-flag-annotation-design.md | Design spec for annotations + both Detekt rules and their intended behavior. |
| docs/superpowers/plans/2026-03-23-behind-flag-annotation.md | Step-by-step implementation plan for the feature. |
| docs/superpowers/specs/2026-03-22-agp9-migration-design.md | Adds AGP 9 migration design doc (separate initiative). |
| docs/superpowers/plans/2026-03-22-agp9-migration.md | Adds AGP 9 migration implementation plan (separate initiative). |
| @@ -0,0 +1,384 @@ | |||
| package dev.androidbroadcast.featured.detekt | |||
|
|
|||
| import io.github.detekt.test.utils.createEnvironment | |||
There was a problem hiding this comment.
createEnvironment is imported from io.github.detekt..., while the rest of the detekt test utilities in this module come from io.gitlab.arturbosch.detekt.test.* (and the dependency is io.gitlab.arturbosch.detekt:detekt-test). This import is likely wrong and can break compilation. Use the createEnvironment helper provided by the detekt-test artifact (the io.gitlab.arturbosch.detekt.test... package) or adjust dependencies accordingly so the import resolves.
| import io.github.detekt.test.utils.createEnvironment | |
| import io.gitlab.arturbosch.detekt.test.createEnvironment |
| super.visit(root) | ||
|
|
There was a problem hiding this comment.
visit() calls super.visit(root) and then performs its own full-tree scans via collectDescendantsOfType. Since this rule doesn’t rely on per-node visitor callbacks, the super.visit(root) traversal is redundant and doubles work per file. Consider removing the super.visit(root) call (or switching to overriding specific visit* methods) to avoid an extra PSI walk.
| super.visit(root) |
| // Pass 1: collect @LocalFlag / @RemoteFlag property names in this file | ||
| val knownFlags = | ||
| root | ||
| .collectDescendantsOfType<KtProperty>() | ||
| .filter { property -> | ||
| property.annotationEntries.any { | ||
| it.shortName?.asString() in setOf("LocalFlag", "RemoteFlag") | ||
| } | ||
| }.mapNotNull { it.name } | ||
| .toSet() |
There was a problem hiding this comment.
knownFlags is populated purely from @LocalFlag/@RemoteFlag property names, without verifying that the property is actually a ConfigParam (or a boolean flag). This can allow @BehindFlag/@AssumesFlag to “validate” against an unrelated, wrongly-typed property annotated by mistake, and it also doesn’t match the PR description that mentions wrong-type ConfigParam references. Consider filtering the collected properties with property.isConfigParam() (and, if needed, additional checks under type resolution) so only real flags are considered valid targets.
| private fun DeclarationDescriptor.behindFlagNameViaPsi(): String? { | ||
| val psi = | ||
| ((this as? DeclarationDescriptorWithSource)?.source as? KotlinSourceElement)?.psi | ||
| ?: return null | ||
| return when (psi) { | ||
| is KtNamedFunction -> psi.annotationEntries.behindFlagName() | ||
| is KtClassOrObject -> psi.annotationEntries.behindFlagName() | ||
| is KtProperty -> psi.annotationEntries.behindFlagName() | ||
| else -> null | ||
| } |
There was a problem hiding this comment.
behindFlagNameViaPsi() only handles KtNamedFunction, KtClassOrObject, and KtProperty. For constructor calls, the resolved descriptor’s PSI source can be a KtPrimaryConstructor (especially when the class declares an explicit primary constructor), which would currently return null and skip reporting. To make class-level @BehindFlag enforcement reliable, handle constructor PSI (e.g., KtPrimaryConstructor → parent KtClassOrObject) and/or, when the resulting descriptor is a constructor, check the containing class descriptor/PSI for @BehindFlag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scope UncheckedFlagAccess to same-file only (consistent with all existing rules, avoids FQN/lifecycle ambiguity without type resolution) - Specify two-pass accumulator algorithm for ordering safety - Add companion object exclusion corner case and test coverage - Document Debt constants, koverVerify note, FeaturedRuleSetProvider KDoc update Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KMP project requires cross-file analysis. UncheckedFlagAccess now uses BindingContext via detektWithTypeResolution task. InvalidFlagReference remains PSI-only. Documents BindingContext.EMPTY guard and Gradle setup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PSI-only two-pass rule that catches typos in @BehindFlag/@AssumesFlag flag name arguments at lint time, with no false positives when the flag registry lives in a separate file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…action Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Warns when @BehindFlag-annotated functions or constructors are called outside a feature-flag guard (if/when check, or @BehindFlag/@AssumesFlag scope). Uses BindingContext for cross-module detection, with PSI fallback for unit-test environments where the annotation is not on the classpath. 15 tests, all green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, add lambda and property tests - FeaturedRuleSetProvider: add KDoc documenting all 5 rules with detekt.yml example; register InvalidFlagReference(config) - UncheckedFlagAccess: add visitSimpleNameExpression to detect @BehindFlag-annotated property accesses; update behindFlagNameViaPsi() to handle KtProperty - UncheckedFlagAccessTest: add two tests — lambda body calling @BehindFlag function (17th test) and @BehindFlag property access without guard (17 total, all passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…llable references - Change KtClass → KtClassOrObject in isInValidFlagContext so that standalone `object` declarations annotated with @AssumesFlag/@BehindFlag are recognised as valid guard scopes (companion objects remain excluded). - Update hasGuardAnnotation extension receiver from KtClass to KtClassOrObject. - Remove now-unused KtClass import. - Document that callable references to @BehindFlag declarations outside a guard are intentionally reported as violations. - Add 2 new tests (19 total): object-scope guard and callable-reference finding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
59baf69 to
44ea441
Compare
Summary
@BehindFlagand@AssumesFlagannotations to:corefor declaring flag dependencies between classes/functionsInvalidFlagReferenceDetekt rule — reports@BehindFlag/@AssumesFlagreferences to non-existent or wrong-typeConfigParampropertiesUncheckedFlagAccessDetekt rule — reports access to a@BehindFlag-annotated declaration without checking the referenced flag, with full type resolutionTest Plan
./gradlew :featured-detekt-rules:test— all tests pass./gradlew :core:build— annotations compile on all KMP targets./gradlew :featured-detekt-rules:check— BCV and spotless pass🤖 Generated with Claude Code