Skip to content

OptionalNotPresentToIsEmpty rewrites !isPresent() on a non-java.util.Optional type, producing uncompilable isEmpty() #1146

Description

@sergeykad

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions