What version of OpenRewrite are you using?
- Gradle plugin
org.openrewrite:plugin v7.35.0
- rewrite-migrate-java v3.38.0
- Active recipe:
org.openrewrite.java.migrate.UpgradeToJava21 (which pulls in org.openrewrite.java.migrate.util.OptionalNotPresentToIsEmpty)
How are you running OpenRewrite?
Gradle plugin, applied via an init script over a large multi-module project:
rootProject {
plugins.apply(org.openrewrite.gradle.RewritePlugin)
dependencies {
rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.38.0")
}
rewrite {
activeRecipe("org.openrewrite.java.migrate.UpgradeToJava21")
setExportDatatables(true)
}
}
The affected module depends on Jolt (com.bazaarvoice.jolt:jolt-core:0.1.8), which ships its own
com.bazaarvoice.jolt.common.Optional<T>. That class exposes isPresent() and get() but has no
isEmpty() method (its full public API is empty(), of(T), get(), isPresent(), equals, toString).
The project is private, but the third-party type is public and small; the relevant snippet is below.
What is the smallest, simplest way to reproduce the problem?
The receiver is a Jolt Optional, not a JDK Optional:
import com.bazaarvoice.jolt.common.Optional; // NOT java.util.Optional
class ConvertMarkdownToHtmlRTL {
Optional<Object> applySingle(Object input) {
Optional<Object> baseHtmlOpt = convert(input);
if (!baseHtmlOpt.isPresent()) { // original, compiles fine
return Optional.empty();
}
// ...
}
}
What did you expect to see?
The code left unchanged. baseHtmlOpt is a com.bazaarvoice.jolt.common.Optional, not a
java.util.Optional, so the recipe should not apply. The recipe's MethodMatcher is in fact scoped to the
JDK type — new MethodMatcher("java.util.Optional isPresent()") with template
#{any(java.util.Optional)}.isEmpty()
(OptionalNotPresentToIsEmpty.java, lines 46 and 56 on v3.38.0) — so the intent is clearly JDK-only.
What did you see instead?
The recipe rewrote it to call isEmpty() on the Jolt type, which does not have that method, so the module
no longer compiles:
import com.bazaarvoice.jolt.common.Optional;
class ConvertMarkdownToHtmlRTL {
Optional<Object> applySingle(Object input) {
Optional<Object> baseHtmlOpt = convert(input);
if (baseHtmlOpt.isEmpty()) { // does not compile: no isEmpty() on com.bazaarvoice.jolt.common.Optional
return Optional.empty();
}
// ...
}
}
Because the JDK-scoped MethodMatcher nevertheless matched a com.bazaarvoice.jolt.common.Optional
receiver, this looks like the matcher firing when the receiver's type is unresolved/incompletely attributed
(the third-party Optional not being on the recipe's typed classpath), falling through to a name-only match
on isPresent(). A type-scoped migration recipe should not rewrite when it cannot positively confirm the
receiver is java.util.Optional — otherwise it silently emits uncompilable code with no warning or error from
the run itself.
What is the full stack trace of any errors you encountered?
No stack trace — the recipe run completes without error. The failure is a downstream Java compile error:
error: cannot find symbol
if (baseHtmlOpt.isEmpty()) {
^
symbol: method isEmpty()
location: variable baseHtmlOpt of type com.bazaarvoice.jolt.common.Optional<Object>
Are you interested in contributing a fix to OpenRewrite?
Open to it if you can point me at the preferred guard (e.g. requiring resolved receiver type before applying). Happy to add a recipe test that reproduces the mis-fire on a non-JDK Optional.
What version of OpenRewrite are you using?
org.openrewrite:pluginv7.35.0org.openrewrite.java.migrate.UpgradeToJava21(which pulls inorg.openrewrite.java.migrate.util.OptionalNotPresentToIsEmpty)How are you running OpenRewrite?
Gradle plugin, applied via an init script over a large multi-module project:
rootProject { plugins.apply(org.openrewrite.gradle.RewritePlugin) dependencies { rewrite("org.openrewrite.recipe:rewrite-migrate-java:3.38.0") } rewrite { activeRecipe("org.openrewrite.java.migrate.UpgradeToJava21") setExportDatatables(true) } }The affected module depends on Jolt (
com.bazaarvoice.jolt:jolt-core:0.1.8), which ships its owncom.bazaarvoice.jolt.common.Optional<T>. That class exposesisPresent()andget()but has noisEmpty()method (its full public API isempty(),of(T),get(),isPresent(),equals,toString).The project is private, but the third-party type is public and small; the relevant snippet is below.
What is the smallest, simplest way to reproduce the problem?
The receiver is a Jolt
Optional, not a JDKOptional:What did you expect to see?
The code left unchanged.
baseHtmlOptis acom.bazaarvoice.jolt.common.Optional, not ajava.util.Optional, so the recipe should not apply. The recipe'sMethodMatcheris in fact scoped to theJDK type —
new MethodMatcher("java.util.Optional isPresent()")with template#{any(java.util.Optional)}.isEmpty()(
OptionalNotPresentToIsEmpty.java, lines 46 and 56 onv3.38.0) — so the intent is clearly JDK-only.What did you see instead?
The recipe rewrote it to call
isEmpty()on the Jolt type, which does not have that method, so the moduleno longer compiles:
Because the JDK-scoped
MethodMatchernevertheless matched acom.bazaarvoice.jolt.common.Optionalreceiver, this looks like the matcher firing when the receiver's type is unresolved/incompletely attributed
(the third-party
Optionalnot being on the recipe's typed classpath), falling through to a name-only matchon
isPresent(). A type-scoped migration recipe should not rewrite when it cannot positively confirm thereceiver is
java.util.Optional— otherwise it silently emits uncompilable code with no warning or error fromthe run itself.
ClassCastExceptionthrown by the siblingOptionalNotEmptyToIsPresent); here there is no crash — the recipe completes "successfully" and the breakageonly surfaces at the next
compileJava.What is the full stack trace of any errors you encountered?
No stack trace — the recipe run completes without error. The failure is a downstream Java compile error:
Are you interested in contributing a fix to OpenRewrite?
Open to it if you can point me at the preferred guard (e.g. requiring resolved receiver type before applying). Happy to add a recipe test that reproduces the mis-fire on a non-JDK
Optional.