Add aggregator plugin and GeneratedFeaturedRegistry codegen#198
Conversation
Introduce dev.androidbroadcast.featured.application Gradle plugin that
aggregates featured-manifest.json artifacts from project dependencies
declared via featuredAggregation(project(...)) and generates a unified
object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> } in
build/generated/featured/commonMain/.
The aggregator reuses the FEATURED_MANIFEST_USAGE / schemaMajorAttr
contract published by dev.androidbroadcast.featured (PR A, schema v1)
through a resolvable featuredAggregationClasspath configuration.
generateFeaturedRegistry is @CacheableTask with stable (modulePath, key)
sort order; duplicate keys across the aggregated graph fail the build
naming both module paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Apply finalize Round 1 findings across code-reviewer, /simplify, the pr-review-toolkit trio, and the architecture/build/security experts: - Escape newline / carriage-return / tab in escapeKotlinString so a description or STRING default with literal whitespace cannot break the generated source. - Validate enumTypeFqn against Kotlin FQN grammar and ENUM defaultValue against the Kotlin identifier grammar before codegen. A hostile project dependency can no longer inject Kotlin source through the featured-manifest.json payload that would execute on the next compile in the consumer module. - Sort manifests by modulePath inside validateUniqueKeys so the duplicate-key error lists origins in a deterministic order regardless of Gradle resolution order. - Wrap manifest parse and read errors with the offending file path so diagnostics name which artifact failed. - Thread the offending key + module path into the requireNotNull error raised when an ENUM descriptor is missing its enumTypeFqn. - Validate outputPackage against a Kotlin package-name regex at task execution time instead of waiting for the consumer compile to fail. - Narrow FeaturedApplicationPlugin to internal — Gradle still resolves the descriptor by FQN via reflection, and the consumer-facing surface is the plugin id alone. - Remove the redundant pre-sort and dead Origin class in the task; the generator does its own (modulePath, key) sort, and the validator now owns ordering for its own error path. - Drop a stale inline comment that paraphrased the generator KDoc. Tests: - Newline, carriage return, and tab escape tests for STRING defaults and descriptions. - Parse-error test asserting the wrapper carries the file path. - Descriptor-integrity tests covering enumTypeFqn / defaultValue injection attempts (semicolon, angle bracket, parenthesis, brace, whitespace, Unicode line separator, missing FQN). - Lazy-realization test mirroring the producer-side pattern. :featured-gradle-plugin:check — BUILD SUCCESSFUL, 266 tests, 0 failures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
2 issues found across 22 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt">
<violation number="1" location="featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt:77">
P2: Test claims to verify that accessing the configuration does not eagerly realize the task, but the assertion (`project.tasks.names.contains(...)`) passes regardless of realization state. The test cannot detect a regression if the configuration is later changed to trigger task realization.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| } | ||
|
|
||
| @Test | ||
| fun `accessing featuredAggregationClasspath configuration does not eagerly realize generateFeaturedRegistry task`() { |
There was a problem hiding this comment.
P2: Test claims to verify that accessing the configuration does not eagerly realize the task, but the assertion (project.tasks.names.contains(...)) passes regardless of realization state. The test cannot detect a regression if the configuration is later changed to trigger task realization.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/aggregation/GenerateFeaturedRegistryTaskRegistrationTest.kt, line 77:
<comment>Test claims to verify that accessing the configuration does not eagerly realize the task, but the assertion (`project.tasks.names.contains(...)`) passes regardless of realization state. The test cannot detect a regression if the configuration is later changed to trigger task realization.</comment>
<file context>
@@ -0,0 +1,90 @@
+ }
+
+ @Test
+ fun `accessing featuredAggregationClasspath configuration does not eagerly realize generateFeaturedRegistry task`() {
+ val project = ProjectBuilder.builder().build()
+ project.plugins.apply("dev.androidbroadcast.featured.application")
</file context>
There was a problem hiding this comment.
Acknowledged. The assertion mirrors the PR A pattern (FeaturedManifestConfigurationTest.accessing featuredManifest configuration does not eagerly realize generateFeaturedManifest task), kept for cross-test consistency. Gradle does not expose a public TaskProvider.isRealized predicate, so a stronger assertion would either rely on internal APIs or on indirect signals (e.g. tasks.size deltas) that are themselves brittle. A project-wide upgrade to a stricter lazy-realization assertion is worth a dedicated cleanup PR rather than a divergent pattern in this one.
There was a problem hiding this comment.
Thanks for the feedback! I've saved this as a new learning to improve future reviews.
There was a problem hiding this comment.
Pull request overview
Adds the consumer-side aggregation plugin for Featured manifests, enabling an application/aggregator module to resolve published featured-manifest.json artifacts and generate a unified GeneratedFeaturedRegistry.
Changes:
- Adds
dev.androidbroadcast.featured.applicationGradle plugin, aggregation configurations, andgenerateFeaturedRegistry. - Adds registry code generation, duplicate-key validation, enum descriptor integrity validation, and parse-error wrapping.
- Adds unit/TestKit coverage plus a multi-module Android fixture and changelog entry.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
featured-gradle-plugin/build.gradle.kts |
Registers the new application plugin ID. |
FeaturedApplicationPlugin.kt |
Creates aggregation configurations and wires the registry generation task. |
AggregationContract.kt |
Defines shared aggregation names and generated registry constants. |
GenerateFeaturedRegistryTask.kt |
Parses manifests, validates descriptors, and writes generated source. |
GeneratedFeaturedRegistryGenerator.kt |
Emits GeneratedFeaturedRegistry.kt source from manifest descriptors. |
GenerateFeaturedRegistryTaskRegistrationTest.kt |
Covers task registration defaults. |
GeneratedFeaturedRegistryGeneratorTest.kt |
Covers generated source formatting and escaping. |
FeaturedAggregationParseErrorTest.kt |
Covers malformed manifest error reporting. |
FeaturedAggregationIntegrationTest.kt |
Adds TestKit aggregation fixture coverage. |
FeaturedAggregationDuplicateKeyTest.kt |
Covers duplicate flag-key validation. |
FeaturedAggregationDescriptorIntegrityTest.kt |
Covers enum descriptor validation. |
FeaturedAggregationConfigurationTest.kt |
Covers configuration roles and attributes. |
aggregator-multi-module-project/settings.gradle.kts |
Defines the integration fixture build. |
aggregator-multi-module-project/gradle.properties |
Enables fixture Android/configuration-cache settings. |
feature-profile/AndroidManifest.xml |
Adds fixture Android manifest. |
feature-profile/build.gradle.kts |
Adds fixture feature flags. |
feature-checkout/AndroidManifest.xml |
Adds fixture Android manifest. |
feature-checkout/build.gradle.kts |
Adds fixture feature flags including enum. |
aggregator-multi-module-project/build.gradle.kts |
Adds fixture root build file. |
app/AndroidManifest.xml |
Adds fixture app manifest. |
app/build.gradle.kts |
Applies aggregation plugin and declares feature aggregation dependencies. |
CHANGELOG.md |
Documents the new aggregation plugin. |
| private fun FlagDescriptor.toDefaultLiteral(): String = | ||
| when (valueType) { |
| val typeArg = descriptor.valueType.toKotlinTypeName(descriptor.enumTypeFqn) | ||
| val defaultLiteral = descriptor.toDefaultLiteral() |
| * KMP-safe: imports only `dev.androidbroadcast.featured.ConfigParam` and enum FQNs | ||
| * (one import per distinct enum). All type arguments and default values for ENUM flags use | ||
| * the fully-qualified class name inline so the generated file compiles without requiring | ||
| * the enum types on the plugin's own classpath. |
cubic #1 and Copilot #3: extend validateFlagDescriptorIntegrity from ENUM-only to an exhaustive when over all seven ValueTypes. Boolean, Int, Long, Float, and Double defaultValue strings are now validated against their respective Kotlin literal grammars before reaching the codegen. A malicious project dependency can no longer smuggle Kotlin through a primitive default like "0.also { ... }" for an Int flag or "false; init { ... }; private val x = true" for a Boolean. STRING is the only remaining unchecked branch — already neutralised by escapeKotlinString. Copilot #4: document that featuredAggregation(project(...)) resolves only the featured-manifest variant, not the producer's main runtime. Modules with enum flags additionally require implementation(project) in the consumer for the enum class to be on the compile classpath. Plugin KDoc and the CHANGELOG entry both call this out. Copilot #5: fix GeneratedFeaturedRegistryGenerator KDoc that claimed to import enum FQNs. The generator imports only ConfigParam and references enum types inline by FQN; KDoc now matches. Tests: 10 new primitive-literal cases — true/false pass, injection shapes throw, negative ints pass, Long max pass, scientific-notation double pass, brace/method-call shapes throw. :featured-gradle-plugin:check — BUILD SUCCESSFUL, 0 failures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
What changed
Second PR in a 4-PR Featured redesign. Adds a new Gradle plugin
dev.androidbroadcast.featured.application(hosted in the existing:featured-gradle-pluginmodule as a second plugin ID). The plugin:featuredAggregationforfeaturedAggregation(project(":feature:checkout"))entries.featuredAggregationClasspaththat carries the sameUsage = featured-manifest+schema-major = 1attributes published bydev.androidbroadcast.featuredin PR A (Publish per-module Featured manifest (producer side) #197).@CacheableTask generateFeaturedRegistrythat parses every resolvedfeatured-manifest.json, validates uniqueness of flag keys across the aggregated graph (and validates ENUM descriptors against Kotlin grammar — see Security), and emitsobject GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> = listOf(...) }tobuild/generated/featured/commonMain/.Featured does not keep API compatibility; the legacy
FlagRegistry/GeneratedFlagRegistrarpipeline stays untouched in PR B and is removed in PR C.Why
PR A made each Featured module publish its flags as a JSON manifest. PR B is the consumer side: a single aggregator surface that lets the application module collect all feature-module manifests into one strongly-typed
List<ConfigParam<*>>instead of hand-registering them at startup. The aggregator is the foundation for the UI-agnostic registry used by debug screens and tooling in PR C / D.Design decisions
generateFeaturedRegistryparses + validates + codegens in a single@CacheableTask. Avalidate/generatesplit was considered and rejected as pragmatic noise (revisit if a CI use-case appears).featuredAggregation(project(...)). Hilt-style classpath scanning is out of scope.kotlin.sourceSets.commonMain.kotlin.srcDir(...)(ormainfor plain JVM / AGP) — matches the existing convention used bygenerateConfigParamandgenerateFlagRegistrar.(modulePath, key)so identical input always produces byte-identical output (cache-key correctness + reproducible source diffs). Duplicate-key error message likewise sorts origins by module path.Security
The codegen interpolates strings from per-module manifests into a generated
.ktfile. STRING-field interpolation goes throughescapeKotlinString(handles\,",$,\n,\r,\t). ENUMenumTypeFqnanddefaultValueare embedded as Kotlin identifiers (not string literals) — those are validated against[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*and[A-Za-z_][A-Za-z0-9_]*regexes before codegen. A malicious project dependency can no longer smuggle Kotlin source through manifest content. Coverage: 9 descriptor-integrity tests covering;,>,(,{, whitespace, U+2028, and the missing-FQN case.How to test
The aggregation suite adds ~30 tests (generator unit, task registration, configuration roles, duplicate-key validator, descriptor integrity, parse-error wrapping, multi-module integration via TestKit + AGP 9.1.0). Integration tests skip silently when
ANDROID_HOME/ANDROID_SDK_ROOTis not set.Multi-module gate against the in-tree fixture:
Artifacts
swarm-report/pr-b-aggregator-plan.mdswarm-report/pr-b-aggregator-finalize.md(Round 1 PASS, all BLOCKs across phases A → D resolved)swarm-report/research/research-flag-registry-autogen.md(decision Approach A — Gradle aggregation + split plugin)Release Notes
Already merged into
CHANGELOG.mdunder## [Unreleased].Status
:featured-gradle-plugin:checkspotlessCheck/finalizeRound 1Non-goals (deferred to follow-up PRs)
FlagRegistry/GeneratedFlagRegistrar(PR C).implementation/runtimeClasspathextension.sincefield onFlagDescriptor(schema v1 does not carry it).Checklist
## [Unreleased]:sample:android-appafter merge (deferred to PR D)🤖 Generated with Claude Code