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

    No labels
    No labels

    Type

    No type

    Projects

    Status
    In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions