Skip to content

Kotlin recipe DSL: drop KotlinCompositeRecipe bridge row from recipes.csv#7710

Merged
jkschneider merged 1 commit into
mainfrom
kotlin-composite-recipe-cleanup
May 17, 2026
Merged

Kotlin recipe DSL: drop KotlinCompositeRecipe bridge row from recipes.csv#7710
jkschneider merged 1 commit into
mainfrom
kotlin-composite-recipe-cleanup

Conversation

@jkschneider
Copy link
Copy Markdown
Member

@jkschneider jkschneider commented May 17, 2026

Summary

rewrite-build-gradle-plugin 2.17.0 (built against rewrite-core 8.82.0) ships the @AbstractRecipe filter — but the released RecipeClassLoader doesn't include org.openrewrite.AbstractRecipe in PARENT_DELEGATED_PREFIXES. With child-first delegation, the annotation type on KotlinCompositeRecipe resolves through the recipe-JAR classpath's rewrite-core while the AbstractRecipe.class literal inside ClasspathScanningLoader resolves through the plugin's own classpath — two distinct Class<?> objects, and Class.isAnnotationPresent returns false. The annotation alone is therefore not yet sufficient to skip the class.

To unblock the cleanup without waiting for another plugin release, this PR:

  • Adds a classloader-agnostic backstop: KotlinCompositeRecipe's @JsonCreator constructor now require(displayName != null). ClasspathScanningLoader.configureRecipe catches Throwable, so the no-arg instantiability probe trips the require and the class is silently skipped.
  • Removes the org.openrewrite.KotlinCompositeRecipe row from rewrite-kotlin/recipes.csv.

Once a future rewrite-core release adds org.openrewrite.AbstractRecipe to PARENT_DELEGATED_PREFIXES (and a new build plugin picks it up), the annotation will work standalone and the init { require(...) } becomes belt-and-suspenders. Removing the init later is optional — the annotation will already filter the class.

Test plan

  • ./gradlew :rewrite-kotlin:recipeCsvValidate passes locally with rewrite-build-gradle-plugin 2.17.0 and rewrite-core 8.82.0
  • RecipeDslSurfaceTest still passes — recipes(...) runtime factory still constructs KotlinCompositeRecipe with a real displayName
  • RecipePluginRewriteTest still passes — synthesized <Name>$KtRecipe classes are unaffected

…es.csv

`rewrite-build-gradle-plugin 2.17.0` (with `rewrite-core 8.82.0`) honors the
`@AbstractRecipe` filter, but only when the annotation type loads through the
same classloader as the scanner's class literal. The released
`RecipeClassLoader` does child-first lookup for `org.openrewrite.AbstractRecipe`,
so the recipe-JAR classloader resolves it from the in-classpath `rewrite-core`
while the scanner resolves it from the plugin's own classpath — two different
`Class<?>` objects, and `Class.isAnnotationPresent` returns false.

Adding a classloader-agnostic backstop: `KotlinCompositeRecipe`'s
`@JsonCreator` constructor now refuses the no-arg probe via `require(displayName
!= null)`. `ClasspathScanningLoader.configureRecipe` catches `Throwable`, so the
class is silently skipped during the marketplace scan. Once the eventual
rewrite-core release adds `org.openrewrite.AbstractRecipe` to
`PARENT_DELEGATED_PREFIXES` (and a new build plugin picks it up), the annotation
alone will suffice and the `init` check becomes belt-and-suspenders.

With the constructor backstop in place, the bridge row for
`org.openrewrite.KotlinCompositeRecipe` in `rewrite-kotlin/recipes.csv` is no
longer needed and is removed here.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite May 17, 2026
@jkschneider jkschneider merged commit f4fcf39 into main May 17, 2026
1 check passed
@jkschneider jkschneider deleted the kotlin-composite-recipe-cleanup branch May 17, 2026 22:21
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 17, 2026
jkschneider added a commit that referenced this pull request May 17, 2026
* RecipeClassLoader: parent-delegate `@AbstractRecipe` for cross-classloader filter

The `@AbstractRecipe` annotation introduced in 8.82.0 lets recipe classes opt
out of classpath-scanning enumeration. The filter is implemented in
`ClasspathScanningLoader` via `Class.isAnnotationPresent(AbstractRecipe.class)`
— a `Class<?>` identity check.

In a marketplace scan (`MavenRecipeBundleReader.marketplaceFromClasspathScan`),
the recipe JAR is loaded by a `RecipeClassLoader` whose parent is the
scanner's classloader. The recipe class's annotation type is resolved via the
recipe-JAR classloader; the scanner's `AbstractRecipe.class` literal resolves
via the scanner's classloader. With child-first delegation, these can be two
distinct `Class<?>` objects whenever both classloaders provide
`org.openrewrite.AbstractRecipe` — and `isAnnotationPresent` then returns
false despite the annotation being present on the bytecode.

Adding `org.openrewrite.AbstractRecipe` to `PARENT_DELEGATED_PREFIXES` makes
both lookups resolve through the same parent classloader, restoring identity.

With this change, `KotlinCompositeRecipe`'s `init { require(displayName !=
null) }` backstop (added in #7710) becomes redundant for any scanner using
this version of `RecipeClassLoader` — but it stays in place as defense in
depth for older plugin versions still in the wild.

* KotlinCompositeRecipe: FIXME the `init` backstop now obsoleted by parent delegation

With `org.openrewrite.AbstractRecipe` now in `RecipeClassLoader.PARENT_DELEGATED_PREFIXES`,
scanners running this version of `rewrite-core` see the annotation correctly
and skip `KotlinCompositeRecipe` without help from the constructor check. Tag
the `init { require(...) }` with a FIXME so it can be removed once older
plugin/CLI versions are no longer in circulation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant