diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/OptionalOrElse.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/OptionalOrElse.java
new file mode 100644
index 0000000000..6e1a83f413
--- /dev/null
+++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/OptionalOrElse.java
@@ -0,0 +1,135 @@
+package tech.picnic.errorprone.bugpatterns;
+
+import static com.google.errorprone.BugPattern.LinkType.NONE;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.errorprone.BugPattern.StandardTags.PERFORMANCE;
+import static com.google.errorprone.matchers.Matchers.instanceMethod;
+import static com.google.errorprone.matchers.Matchers.staticMethod;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.service.AutoService;
+import com.google.common.collect.Iterables;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.fixes.SuggestedFixes;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.refaster.Refaster;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import java.util.Optional;
+import java.util.function.Supplier;
+import tech.picnic.errorprone.utils.SourceCode;
+
+/**
+ * A {@link BugChecker} that flags arguments to {@link Optional#orElse(Object)} that should be
+ * deferred using {@link Optional#orElseGet(Supplier)}.
+ *
+ *
The suggested fix assumes that the argument to {@code orElse} does not have side effects. If
+ * it does, the suggested fix changes the program's semantics. Such fragile code must instead be
+ * refactored such that the side-effectful code does not appear accidental.
+ */
+// XXX: Consider also implementing the inverse, in which `.orElseGet(() -> someConstant)` is
+// flagged.
+// XXX: Once the `MethodReferenceUsageCheck` becomes generally usable, consider leaving the method
+// reference cleanup to that check, and express the remainder of the logic in this class using a
+// Refaster template, i.c.w. a `@Matches` constraint that implements the `requiresComputation`
+// logic.
+@AutoService(BugChecker.class)
+@BugPattern(
+ summary =
+ """
+ Prefer `Optional#orElseGet` over `Optional#orElse` if the fallback requires additional \
+ computation""",
+ linkType = NONE,
+ severity = WARNING,
+ tags = PERFORMANCE)
+public final class OptionalOrElse extends BugChecker implements MethodInvocationTreeMatcher {
+ private static final long serialVersionUID = 1L;
+ private static final Matcher OPTIONAL_OR_ELSE_METHOD =
+ instanceMethod().onExactClass(Optional.class.getCanonicalName()).namedAnyOf("orElse");
+ // XXX: Also exclude invocations of `@Placeholder`-annotated methods.
+ private static final Matcher REFASTER_METHOD =
+ staticMethod().onClass(Refaster.class.getCanonicalName());
+
+ /** Instantiates a new {@link OptionalOrElse} instance. */
+ public OptionalOrElse() {}
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (!OPTIONAL_OR_ELSE_METHOD.matches(tree, state)) {
+ return Description.NO_MATCH;
+ }
+
+ ExpressionTree argument = Iterables.getOnlyElement(tree.getArguments());
+ if (!requiresComputation(argument) || REFASTER_METHOD.matches(argument, state)) {
+ return Description.NO_MATCH;
+ }
+
+ /*
+ * We have a match. Construct the method reference or lambda expression to be passed to the
+ * replacement `#orElseGet` invocation.
+ */
+ String newArgument =
+ tryMethodReferenceConversion(argument, state)
+ .orElseGet(() -> "() -> " + SourceCode.treeToString(argument, state));
+
+ /* Construct the suggested fix, replacing the method invocation and its argument. */
+ SuggestedFix fix =
+ SuggestedFix.builder()
+ .merge(SuggestedFixes.renameMethodInvocation(tree, "orElseGet", state))
+ .replace(argument, newArgument)
+ .build();
+
+ return describeMatch(tree, fix);
+ }
+
+ /**
+ * Tells whether the given expression contains anything other than a literal or a (possibly
+ * dereferenced) variable or constant.
+ */
+ private static boolean requiresComputation(ExpressionTree tree) {
+ return !(tree instanceof IdentifierTree
+ || tree instanceof LiteralTree
+ || (tree instanceof MemberSelectTree memberSelect
+ && !requiresComputation(memberSelect.getExpression()))
+ || ASTHelpers.constValue(tree) != null);
+ }
+
+ /** Returns the nullary method reference matching the given expression, if any. */
+ private static Optional tryMethodReferenceConversion(
+ ExpressionTree tree, VisitorState state) {
+ if (!(tree instanceof MethodInvocationTree methodInvocation)) {
+ return Optional.empty();
+ }
+
+ if (!methodInvocation.getArguments().isEmpty()) {
+ return Optional.empty();
+ }
+
+ if (!(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelect)) {
+ return Optional.empty();
+ }
+
+ if (requiresComputation(memberSelect.getExpression())) {
+ return Optional.empty();
+ }
+
+ return Optional.of(
+ SourceCode.treeToString(memberSelect.getExpression(), state)
+ + "::"
+ + (methodInvocation.getTypeArguments().isEmpty()
+ ? ""
+ : methodInvocation.getTypeArguments().stream()
+ .map(arg -> SourceCode.treeToString(arg, state))
+ .collect(joining(",", "<", ">")))
+ + memberSelect.getIdentifier());
+ }
+}
diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/OptionalRules.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/OptionalRules.java
index 1491b18eac..cf1c2cb3ce 100644
--- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/OptionalRules.java
+++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/OptionalRules.java
@@ -360,7 +360,7 @@ Optional after(Optional optional, Function super S, Optional extends R
/** Prefer {@link Optional#or(Supplier)} over more verbose alternatives. */
static final class OptionalOrOtherOptional {
@BeforeTemplate
- @SuppressWarnings("NestedOptionals" /* Auto-fix for the `NestedOptionals` check. */)
+ @SuppressWarnings("NestedOptionals")
Optional before(Optional optional1, Optional optional2) {
// XXX: Note that rewriting the first and third variant will change the code's behavior if
// `optional2` has side-effects.
@@ -386,9 +386,13 @@ Optional after(Optional optional1, Optional optional2) {
*/
static final class OptionalIdentity {
@BeforeTemplate
+ @SuppressWarnings("NestedOptionals")
Optional before(Optional optional, Comparator super T> comparator) {
return Refaster.anyOf(
optional.or(Refaster.anyOf(() -> Optional.empty(), Optional::empty)),
+ optional
+ .map(Optional::of)
+ .orElseGet(Refaster.anyOf(() -> Optional.empty(), Optional::empty)),
optional.stream().findFirst(),
optional.stream().findAny(),
optional.stream().min(comparator),
@@ -442,9 +446,7 @@ Optional extends T> after(Optional optional, Function super S, ? extends
static final class OptionalStream {
@BeforeTemplate
Stream before(Optional optional) {
- return Refaster.anyOf(
- optional.map(Stream::of).orElse(Stream.empty()),
- optional.map(Stream::of).orElseGet(Stream::empty));
+ return optional.map(Stream::of).orElseGet(Stream::empty);
}
@AfterTemplate
diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/OptionalOrElseTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/OptionalOrElseTest.java
new file mode 100644
index 0000000000..620a532ae6
--- /dev/null
+++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/OptionalOrElseTest.java
@@ -0,0 +1,135 @@
+package tech.picnic.errorprone.bugpatterns;
+
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.jupiter.api.Test;
+
+final class OptionalOrElseTest {
+ @Test
+ void identification() {
+ CompilationTestHelper.newInstance(OptionalOrElse.class, getClass())
+ .addSourceLines(
+ "A.java",
+ "import com.google.errorprone.refaster.Refaster;",
+ "import java.util.Optional;",
+ "",
+ "class A {",
+ " private final Optional