Skip to content

feat(detekt): add @BehindFlag/@AssumesFlag annotations and InvalidFlagReference/UncheckedFlagAccess rules#142

Merged
kirich1409 merged 14 commits intomainfrom
feat/behind-flag-annotation
Mar 23, 2026
Merged

feat(detekt): add @BehindFlag/@AssumesFlag annotations and InvalidFlagReference/UncheckedFlagAccess rules#142
kirich1409 merged 14 commits intomainfrom
feat/behind-flag-annotation

Conversation

@kirich1409
Copy link
Contributor

Summary

  • Adds @BehindFlag and @AssumesFlag annotations to :core for declaring flag dependencies between classes/functions
  • Adds InvalidFlagReference Detekt rule — reports @BehindFlag/@AssumesFlag references to non-existent or wrong-type ConfigParam properties
  • Adds UncheckedFlagAccess Detekt rule — reports access to a @BehindFlag-annotated declaration without checking the referenced flag, with full type resolution

Test 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

Copilot AI review requested due to automatic review settings March 23, 2026 12:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 :core for declaring flag-guard expectations.
  • Add two Detekt rules in :featured-detekt-rules: InvalidFlagReference (PSI-only) and UncheckedFlagAccess (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
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
import io.github.detekt.test.utils.createEnvironment
import io.gitlab.arturbosch.detekt.test.createEnvironment

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +52
super.visit(root)

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
super.visit(root)

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +62
// 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()
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +158
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
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
kirich1409 and others added 14 commits March 23, 2026 15:31
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>
@kirich1409 kirich1409 force-pushed the feat/behind-flag-annotation branch from 59baf69 to 44ea441 Compare March 23, 2026 12:32
@kirich1409 kirich1409 merged commit 93ad79b into main Mar 23, 2026
8 of 9 checks passed
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