Kotlin recipe DSL: drop KotlinCompositeRecipe bridge row from recipes.csv#7710
Merged
Conversation
…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.
2 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
recipes()composites +@AbstractRecipe#7707, where we added@AbstractRecipetoKotlinCompositeRecipeand a one-row bridge entry inrewrite-kotlin/recipes.csvsorecipeCsvValidateCompletenesswould keep passing with build plugin versions that didn't yet honor the annotation.rewrite-build-gradle-plugin 2.17.0(built againstrewrite-core 8.82.0) ships the@AbstractRecipefilter — but the releasedRecipeClassLoaderdoesn't includeorg.openrewrite.AbstractRecipeinPARENT_DELEGATED_PREFIXES. With child-first delegation, the annotation type onKotlinCompositeReciperesolves through the recipe-JAR classpath'srewrite-corewhile theAbstractRecipe.classliteral insideClasspathScanningLoaderresolves through the plugin's own classpath — two distinctClass<?>objects, andClass.isAnnotationPresentreturns 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:
KotlinCompositeRecipe's@JsonCreatorconstructor nowrequire(displayName != null).ClasspathScanningLoader.configureRecipecatchesThrowable, so the no-arg instantiability probe trips therequireand the class is silently skipped.org.openrewrite.KotlinCompositeReciperow fromrewrite-kotlin/recipes.csv.Once a future rewrite-core release adds
org.openrewrite.AbstractRecipetoPARENT_DELEGATED_PREFIXES(and a new build plugin picks it up), the annotation will work standalone and theinit { require(...) }becomes belt-and-suspenders. Removing theinitlater is optional — the annotation will already filter the class.Test plan
./gradlew :rewrite-kotlin:recipeCsvValidatepasses locally withrewrite-build-gradle-plugin 2.17.0andrewrite-core 8.82.0RecipeDslSurfaceTeststill passes —recipes(...)runtime factory still constructsKotlinCompositeRecipewith a realdisplayNameRecipePluginRewriteTeststill passes — synthesized<Name>$KtRecipeclasses are unaffected