Skip to content

Add aggregator plugin and GeneratedFeaturedRegistry codegen#198

Merged
kirich1409 merged 3 commits into
developfrom
feat/aggregator-generated-registry
May 19, 2026
Merged

Add aggregator plugin and GeneratedFeaturedRegistry codegen#198
kirich1409 merged 3 commits into
developfrom
feat/aggregator-generated-registry

Conversation

@kirich1409
Copy link
Copy Markdown
Contributor

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-plugin module as a second plugin ID). The plugin:

  • Declares a user-facing declarable configuration featuredAggregation for featuredAggregation(project(":feature:checkout")) entries.
  • Creates an internal resolvable configuration featuredAggregationClasspath that carries the same Usage = featured-manifest + schema-major = 1 attributes published by dev.androidbroadcast.featured in PR A (Publish per-module Featured manifest (producer side) #197).
  • Registers @CacheableTask generateFeaturedRegistry that parses every resolved featured-manifest.json, validates uniqueness of flag keys across the aggregated graph (and validates ENUM descriptors against Kotlin grammar — see Security), and emits object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> = listOf(...) } to build/generated/featured/commonMain/.

Featured does not keep API compatibility; the legacy FlagRegistry / GeneratedFlagRegistrar pipeline 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

  • One task, not two. generateFeaturedRegistry parses + validates + codegens in a single @CacheableTask. A validate / generate split was considered and rejected as pragmatic noise (revisit if a CI use-case appears).
  • Explicit aggregation, no auto-discovery. Consumers declare each project dependency via featuredAggregation(project(...)). Hilt-style classpath scanning is out of scope.
  • Source-set wiring is user responsibility. Generated file lands on disk; the consumer wires the output dir into kotlin.sourceSets.commonMain.kotlin.srcDir(...) (or main for plain JVM / AGP) — matches the existing convention used by generateConfigParam and generateFlagRegistrar.
  • Stable, deterministic output. Descriptors sorted by (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 .kt file. STRING-field interpolation goes through escapeKotlinString (handles \, ", $, \n, \r, \t). ENUM enumTypeFqn and defaultValue are 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

./gradlew :featured-gradle-plugin:check
./gradlew spotlessCheck

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_ROOT is not set.

Multi-module gate against the in-tree fixture:

:app:generateFeaturedRegistry → SUCCESS
:app:build/generated/featured/commonMain/GeneratedFeaturedRegistry.kt — listOf(...) with 4 ConfigParam declarations from :feature-checkout + :feature-profile
second run → UP_TO_DATE
CC store on first run, CC reuse on second run

Artifacts

  • Plan: swarm-report/pr-b-aggregator-plan.md
  • Finalize report: swarm-report/pr-b-aggregator-finalize.md (Round 1 PASS, all BLOCKs across phases A → D resolved)
  • Research baseline: swarm-report/research/research-flag-registry-autogen.md (decision Approach A — Gradle aggregation + split plugin)

Release Notes

### Added

- New `dev.androidbroadcast.featured.application` Gradle plugin: aggregates `featured-manifest.json` artifacts from project dependencies declared via `featuredAggregation(project(...))` and generates `object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> }` in `build/generated/featured/commonMain/`. Apply alongside `dev.androidbroadcast.featured` in the application module; wire the output directory into your source set manually.

Already merged into CHANGELOG.md under ## [Unreleased].

Status

Gate Result
:featured-gradle-plugin:check PASS — 266 tests, 0 failures
spotlessCheck PASS
/finalize Round 1 PASS — Phase A → D, all BLOCKs resolved
Manual E2E in fixture Pending PR D (sample-app integration)

Non-goals (deferred to follow-up PRs)

  • Removing legacy FlagRegistry / GeneratedFlagRegistrar (PR C).
  • Auto-wiring the generated dir into KMP / AGP / JVM source sets (PR D).
  • Auto-discovery via implementation/runtimeClasspath extension.
  • since field on FlagDescriptor (schema v1 does not carry it).

Checklist

  • Tests for happy path + edge cases (empty input, escape characters, descriptor integrity, multi-module)
  • No new dependencies
  • CHANGELOG entry under ## [Unreleased]
  • No changes to PR A code paths (purely additive)
  • Manual smoke against :sample:android-app after merge (deferred to PR D)

🤖 Generated with Claude Code

kirich1409 and others added 2 commits May 19, 2026 10:53
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>
@kirich1409 kirich1409 added enhancement New feature or request gradle-plugin featured-gradle-plugin work testing Tests and coverage labels May 19, 2026
@kirich1409 kirich1409 marked this pull request as ready for review May 19, 2026 09:07
Copilot AI review requested due to automatic review settings May 19, 2026 09:07
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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`() {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 19, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

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 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.application Gradle plugin, aggregation configurations, and generateFeaturedRegistry.
  • 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.

Comment on lines +144 to +145
private fun FlagDescriptor.toDefaultLiteral(): String =
when (valueType) {
Comment on lines +55 to +56
val typeArg = descriptor.valueType.toKotlinTypeName(descriptor.enumTypeFqn)
val defaultLiteral = descriptor.toDefaultLiteral()
Comment on lines +10 to +13
* 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>
@kirich1409 kirich1409 merged commit 30cb9d0 into develop May 19, 2026
9 of 10 checks passed
@kirich1409 kirich1409 deleted the feat/aggregator-generated-registry branch May 19, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request gradle-plugin featured-gradle-plugin work testing Tests and coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants