Skip to content

feat(gradle-plugin): auto-generate FlagRegistry initializers per module#110

Merged
kirich1409 merged 2 commits intomainfrom
feat/issue-20-feat-gradle-plugin-auto-generate-flagreg
Mar 23, 2026
Merged

feat(gradle-plugin): auto-generate FlagRegistry initializers per module#110
kirich1409 merged 2 commits intomainfrom
feat/issue-20-feat-gradle-plugin-auto-generate-flagreg

Conversation

@kirich1409
Copy link
Copy Markdown
Contributor

Summary

  • Adds generateFlagRegistrar Gradle task that reads scanLocalFlags output and emits GeneratedFlagRegistrar.kt with an object GeneratedFlagRegistrar { fun register() { ... } } calling FlagRegistry.register(...) for every @LocalFlag-annotated ConfigParam in the module
  • Extends LocalFlagEntry with propertyName and ownerName fields; adds kotlinReference computed property for compile-safe reference generation
  • Updates LocalFlagScanner regex to capture the Kotlin property name and resolves the nearest enclosing object/companion object as the owner
  • Updates ScanResultParser with backwards-compatible 6-field format (4-field lines from older scan output parse successfully)
  • Generated file is KMP-safe — imports only dev.androidbroadcast.featured.registry.FlagRegistry

AndroidX App Startup integration and app-level aggregator generation are deferred as future work per the issue spec.

Test plan

  • ./gradlew :featured-gradle-plugin:test — all 121 tests pass
  • ./gradlew spotlessCheck — no formatting violations
  • FlagRegistrarGeneratorTest — 11 unit tests covering package, import, object/function generation, register calls for owned/top-level/legacy entries
  • GenerateFlagRegistrarTaskRegistrationTest — 7 tests verifying task registration, type, group, outputFile, packageName, dependency chain, output path
  • LocalFlagEntryKotlinReferenceTest — 4 tests for the kotlinReference computed property
  • LocalFlagScannerPropertyCaptureTest — 7 tests verifying property name and owner capture from scanned source

Closes #20

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 22, 2026 02:59
Copy link
Copy Markdown

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 a new Gradle-plugin capability to generate a per-module Kotlin registrar (GeneratedFlagRegistrar) that registers all @LocalFlag ConfigParams with FlagRegistry, and extends the local-flag scanning/reporting format to support generating compile-time references.

Changes:

  • Add generateFlagRegistrar task + FlagRegistrarGenerator to emit GeneratedFlagRegistrar.kt.
  • Extend LocalFlagEntry and scanning/parsing pipeline to include propertyName + ownerName (with backwards-compatible parsing).
  • Add unit tests covering scanner property/owner capture, Kotlin reference generation, generator output, and task registration.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt Registers the new generateFlagRegistrar task.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTask.kt Implements the task that reads scan output and writes generated Kotlin source.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGenerator.kt Generates the Kotlin source for GeneratedFlagRegistrar.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntry.kt Adds propertyName, ownerName, and kotlinReference to scan entries.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagScanner.kt Updates scanning regex + adds owner-resolution heuristic.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanLocalFlagsTask.kt Updates scan report output to include propertyName and ownerName.
featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/ScanResultParser.kt Adds backwards-compatible support for parsing both 4-field and 6-field scan lines.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagScannerTest.kt Updates scanner tests to assert propertyName/ownerName.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagScannerPropertyCaptureTest.kt Adds tests for property and owner capture behavior.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/LocalFlagEntryKotlinReferenceTest.kt Adds tests for kotlinReference.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/FlagRegistrarGeneratorTest.kt Adds tests for generated registrar source output.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/GenerateFlagRegistrarTaskRegistrationTest.kt Adds tests ensuring the task is registered and configured.

Comment on lines +55 to +70
private fun registerFlagRegistrarGenerationTask(
target: Project,
scanTask: TaskProvider<ScanLocalFlagsTask>,
) {
target.tasks.register(GENERATE_FLAG_REGISTRAR_TASK_NAME, GenerateFlagRegistrarTask::class.java) { task ->
task.group = "featured"
task.description =
"Generates a GeneratedFlagRegistrar.kt source file that registers all " +
"@LocalFlag-annotated ConfigParams from '${target.path}' with FlagRegistry."
task.scanResultFile.set(scanTask.flatMap { it.outputFile })
task.packageName.set("dev.androidbroadcast.featured.generated")
task.outputFile.set(
target.layout.buildDirectory.file("generated/featured/GeneratedFlagRegistrar.kt"),
)
task.dependsOn(scanTask)
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

GenerateFlagRegistrarTask KDoc claims the plugin wires the generated source directory into Kotlin source sets automatically, but FeaturedPlugin currently only registers the task and doesn’t add build/generated/featured as a Kotlin source directory. Either implement the wiring (for KMP/JVM/Android projects) or adjust the documentation to avoid implying the generated registrar will be available without extra build configuration.

Copilot uses AI. Check for mistakes.
Comment on lines 14 to 23
@@ -17,12 +22,30 @@ internal fun RegularFileProperty.parseLocalFlagEntries(): List<LocalFlagEntry> {
.filter { it.isNotBlank() }
.mapNotNull { line ->
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The KDoc says the function returns an empty list when the file "contains lines that do not conform", but the implementation actually skips non-conforming lines and still returns any successfully parsed entries. Either tighten the wording (e.g., "ignores lines that do not conform") or change the implementation to fail the whole parse if any line is malformed.

Copilot uses AI. Check for mistakes.
appendLine(" * [dev.androidbroadcast.featured.ConfigParam] declarations from this module")
appendLine(" * with [FlagRegistry].")
appendLine(" *")
appendLine(" * Call this once at application startup, e.g. from `Application.onCreate()`.")
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The generated KDoc suggests calling GeneratedFlagRegistrar.register() from Application.onCreate(), which is Android-specific. Since this file is intended to be KMP-safe and usable from common code, consider rewording to a platform-agnostic suggestion (e.g., "call once during app startup") to avoid implying an Android-only integration path.

Suggested change
appendLine(" * Call this once at application startup, e.g. from `Application.onCreate()`.")
appendLine(" * Call this once during application startup from your platform's initialization code.")

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +48
/**
* Matches the simple name of an enclosing `object` or `companion object` declaration.
* Used to find the nearest enclosing owner of a `@LocalFlag` property.
*
* Group 1: object name (e.g. `"NewCheckoutFlags"` from `object NewCheckoutFlags {`).
* `companion object` is matched but yields an empty group 1.
*/
private val OBJECT_DECL_REGEX =
Regex(
"""(?:companion\s+object|object\s+(\w+))\s*(?::[^{]*)?\{""",
setOf(RegexOption.MULTILINE),
)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

OBJECT_DECL_REGEX intentionally matches companion object but discards its owner name (group 1 is empty), which means ownerName becomes null and the generated reference becomes unqualified. Flags declared inside a companion object will therefore generate invalid references (they need to be qualified with the enclosing class/object). Consider capturing the enclosing class name for companion objects (or at least emitting a distinct sentinel so the generator can qualify correctly).

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +116
* Builds a sorted list of (startOffset, ownerName?) pairs representing each
* `object` or `companion object` block found in [source].
* Used by [resolveOwner] to determine the enclosing owner of a property.
*/
private fun buildOwnerIndex(source: String): List<Pair<Int, String?>> =
OBJECT_DECL_REGEX
.findAll(source)
.map { m ->
val name = m.groupValues[1].takeIf { it.isNotEmpty() }
m.range.first to name
}.sortedBy { it.first }
.toList()

/**
* Returns the name of the nearest `object` or `companion object` whose opening `{`
* appears before [offset], or `null` if no such enclosing object is found.
*
* This is a heuristic: it picks the last object declaration whose start offset is
* less than [offset], which covers the common pattern where `@LocalFlag` properties
* are declared directly inside a single-level `object` block.
*/
private fun List<Pair<Int, String?>>.resolveOwner(offset: Int): String? = lastOrNull { (start, _) -> start < offset }?.second
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

resolveOwner() currently picks the last object/companion object whose opening { appears before the flag’s offset, without verifying that the flag is actually inside that object's brace range. This will mis-attribute ownerName for top-level flags that appear after any object declaration, and for flags in an outer object after an inner object closes (nested objects). This can lead to incorrect generated references that won’t compile or will point at the wrong symbol; consider tracking brace depth to compute (start,end) ranges for object blocks and only returning an owner when offset falls within the range.

Suggested change
* Builds a sorted list of (startOffset, ownerName?) pairs representing each
* `object` or `companion object` block found in [source].
* Used by [resolveOwner] to determine the enclosing owner of a property.
*/
private fun buildOwnerIndex(source: String): List<Pair<Int, String?>> =
OBJECT_DECL_REGEX
.findAll(source)
.map { m ->
val name = m.groupValues[1].takeIf { it.isNotEmpty() }
m.range.first to name
}.sortedBy { it.first }
.toList()
/**
* Returns the name of the nearest `object` or `companion object` whose opening `{`
* appears before [offset], or `null` if no such enclosing object is found.
*
* This is a heuristic: it picks the last object declaration whose start offset is
* less than [offset], which covers the common pattern where `@LocalFlag` properties
* are declared directly inside a single-level `object` block.
*/
private fun List<Pair<Int, String?>>.resolveOwner(offset: Int): String? = lastOrNull { (start, _) -> start < offset }?.second
* Represents a single `object` or `companion object` block in the source.
*
* @property start Inclusive offset of the opening `{`.
* @property end Inclusive offset of the matching closing `}`.
* @property name Simple object name, or `null` for `companion object`.
*/
private data class OwnerBlock(
val start: Int,
val end: Int,
val name: String?,
)
/**
* Builds a sorted list of [OwnerBlock] entries representing each
* `object` or `companion object` block found in [source].
* Used by [resolveOwner] to determine the enclosing owner of a property.
*/
private fun buildOwnerIndex(source: String): List<OwnerBlock> {
val owners = mutableListOf<OwnerBlock>()
OBJECT_DECL_REGEX
.findAll(source)
.forEach { match ->
// Determine the simple name of the object, if present.
val name = match.groupValues[1].takeIf { it.isNotEmpty() }
// Find the opening brace for this object.
val openingBraceIndex = source.indexOf('{', startIndex = match.range.first)
if (openingBraceIndex == -1) {
// Malformed object declaration; skip.
return@forEach
}
var depth = 0
var i = openingBraceIndex
var closingBraceIndex = -1
// Walk forward to find the matching closing brace for this object block.
while (i < source.length) {
when (source[i]) {
'{' -> {
depth++
}
'}' -> {
depth--
if (depth == 0) {
closingBraceIndex = i
break
}
}
}
i++
}
if (closingBraceIndex != -1) {
owners += OwnerBlock(
start = openingBraceIndex,
end = closingBraceIndex,
name = name,
)
}
}
return owners.sortedBy { it.start }
}
/**
* Returns the name of the nearest `object` or `companion object` whose block
* actually encloses [offset], or `null` if no such enclosing object is found.
*/
private fun List<OwnerBlock>.resolveOwner(offset: Int): String? =
lastOrNull { block -> block.start < offset && offset <= block.end }
?.name

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +67
appendLine("package $packageName")
appendLine()
appendLine("import $FLAG_REGISTRY_IMPORT")
appendLine()
appendLine("// Auto-generated by the Featured Gradle plugin — do not edit manually.")
appendLine("public object GeneratedFlagRegistrar {")
appendLine(" /**")
appendLine(" * Registers all `@LocalFlag`-annotated")
appendLine(" * [dev.androidbroadcast.featured.ConfigParam] declarations from this module")
appendLine(" * with [FlagRegistry].")
appendLine(" *")
appendLine(" * Call this once at application startup, e.g. from `Application.onCreate()`.")
appendLine(" */")
appendLine(" public fun register() {")
entries.forEach { entry ->
val ref = entry.kotlinReference
if (ref.isNotBlank()) {
appendLine(" FlagRegistry.register($ref)")
} else {
appendLine(" // TODO: register flag '${entry.key}' — property reference unavailable")
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The generated registrar uses unqualified references like MyFlags.flagA / darkTheme but the generated file only imports FlagRegistry and is placed in a fixed package. Unless every flag lives in the same package (and is otherwise in scope), this will not compile; the generator needs to either emit fully-qualified references (requires capturing each flag’s package and, for companion objects, enclosing class name) or generate appropriate imports per entry.

Copilot uses AI. Check for mistakes.
appendLine(" *")
appendLine(" * Call this once at application startup, e.g. from `Application.onCreate()`.")
appendLine(" */")
appendLine(" public fun register() {")
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The generated public fun register() is missing an explicit return type. Many modules in this repo enable Kotlin explicitApi(), which requires public functions to declare their return type; the generated source should use public fun register(): Unit to avoid compilation failures in explicit-API modules.

Suggested change
appendLine(" public fun register() {")
appendLine(" public fun register(): Unit {")

Copilot uses AI. Check for mistakes.
Comment on lines 74 to 76
entries.joinToString("\n") { e ->
"${e.key}|${e.defaultValue}|${e.type}|${e.moduleName}"
"${e.key}|${e.defaultValue}|${e.type}|${e.moduleName}|${e.propertyName}|${e.ownerName ?: ""}"
},
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

ScanLocalFlagsTask now writes 6 fields (...|propertyName|ownerName) but the report format documentation in this task (and downstream tasks) still describes the 4-field format. Update the KDoc to reflect the new output so external consumers and future tasks don’t parse the report incorrectly.

Copilot uses AI. Check for mistakes.
kirich1409 and others added 2 commits March 23, 2026 15:27
Adds a new `generateFlagRegistrar` Gradle task that reads the scan results
from `scanLocalFlags` and emits a `GeneratedFlagRegistrar.kt` source file
containing an `object GeneratedFlagRegistrar { fun register() { ... } }`
that calls `FlagRegistry.register(...)` for every `@LocalFlag`-annotated
ConfigParam in the module.

To support compile-safe references, `LocalFlagEntry` gains `propertyName`
and `ownerName` fields. `LocalFlagScanner` now captures the Kotlin property
name and resolves the nearest enclosing `object` or `companion object`.
`ScanResultParser` is updated with a backwards-compatible 6-field format
(4-field lines from older scan output still parse successfully).

The generated file is placed in `build/generated/featured/` and is KMP-safe
(no Android-specific imports). AndroidX App Startup integration and an
app-level aggregator are deferred as future work per the issue spec.

Closes #20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…form neutrality

- Remove false claim that FeaturedPlugin auto-wires the generated source
  directory; consumers must add the srcDir manually (GenerateFlagRegistrarTask)
- Fix ScanResultParser KDoc: non-conforming lines are silently ignored, not
  cause an empty-list return
- Replace Android-specific "Application.onCreate()" startup hint in the
  generated KDoc with a platform-agnostic "during app startup" phrasing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kirich1409 kirich1409 force-pushed the feat/issue-20-feat-gradle-plugin-auto-generate-flagreg branch from fde78fe to adb128b Compare March 23, 2026 12:28
@kirich1409 kirich1409 merged commit 33e0dfc 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.

feat(gradle-plugin): auto-generate FlagRegistry initializers per module

2 participants