-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve Optional#orElse{,Get}
support
#1283
Changes from all commits
23cae0e
2a6fd00
26e8788
8b22398
69d9d62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,14 +18,12 @@ | |
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.refaster.matchers.RequiresComputation; | ||
import tech.picnic.errorprone.utils.SourceCode; | ||
|
||
/** | ||
|
@@ -36,12 +34,12 @@ | |
* 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. | ||
// XXX: This rule may introduce a compilation error: the `value` expression may reference a | ||
// non-effectively final variable, which is not allowed in the replacement lambda expression. | ||
// Review whether a `@Matcher` can be used to avoid this. | ||
// XXX: Once the `MethodReferenceUsageCheck` bug checker 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 `@NotMatches(RequiresComputation.class)` constraint. | ||
@AutoService(BugChecker.class) | ||
@BugPattern( | ||
summary = | ||
|
@@ -51,16 +49,17 @@ | |
linkType = NONE, | ||
severity = WARNING, | ||
tags = PERFORMANCE) | ||
public final class OptionalOrElse extends BugChecker implements MethodInvocationTreeMatcher { | ||
public final class OptionalOrElseGet extends BugChecker implements MethodInvocationTreeMatcher { | ||
private static final long serialVersionUID = 1L; | ||
private static final Matcher<ExpressionTree> REQUIRES_COMPUTATION = new RequiresComputation(); | ||
private static final Matcher<ExpressionTree> OPTIONAL_OR_ELSE_METHOD = | ||
instanceMethod().onExactClass(Optional.class.getCanonicalName()).namedAnyOf("orElse"); | ||
// XXX: Also exclude invocations of `@Placeholder`-annotated methods. | ||
private static final Matcher<ExpressionTree> REFASTER_METHOD = | ||
staticMethod().onClass(Refaster.class.getCanonicalName()); | ||
|
||
/** Instantiates a new {@link OptionalOrElse} instance. */ | ||
public OptionalOrElse() {} | ||
/** Instantiates a new {@link OptionalOrElseGet} instance. */ | ||
public OptionalOrElseGet() {} | ||
|
||
@Override | ||
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { | ||
|
@@ -69,7 +68,8 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState | |
} | ||
|
||
ExpressionTree argument = Iterables.getOnlyElement(tree.getArguments()); | ||
if (!requiresComputation(argument) || REFASTER_METHOD.matches(argument, state)) { | ||
if (!REQUIRES_COMPUTATION.matches(argument, state) | ||
|| REFASTER_METHOD.matches(argument, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
|
@@ -91,18 +91,6 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState | |
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); | ||
} | ||
Comment on lines
-94
to
-104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of two not-quite-complimentary implementations, all logic now lives in the |
||
|
||
/** Returns the nullary method reference matching the given expression, if any. */ | ||
private static Optional<String> tryMethodReferenceConversion( | ||
ExpressionTree tree, VisitorState state) { | ||
|
@@ -118,7 +106,7 @@ private static Optional<String> tryMethodReferenceConversion( | |
return Optional.empty(); | ||
} | ||
|
||
if (requiresComputation(memberSelect.getExpression())) { | ||
if (REQUIRES_COMPUTATION.matches(memberSelect.getExpression(), state)) { | ||
return Optional.empty(); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,7 @@ | |
import java.util.stream.Stream; | ||
import org.jspecify.annotations.Nullable; | ||
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation; | ||
import tech.picnic.errorprone.refaster.matchers.IsLikelyTrivialComputation; | ||
import tech.picnic.errorprone.refaster.matchers.RequiresComputation; | ||
|
||
/** Refaster rules related to expressions dealing with {@link Optional}s. */ | ||
@OnlineDocumentation | ||
|
@@ -255,24 +255,21 @@ T after(Optional<T> o1, Optional<T> o2) { | |
} | ||
|
||
/** | ||
* Prefer {@link Optional#orElseGet(Supplier)} over {@link Optional#orElse(Object)} if the | ||
* fallback value is not the result of a trivial computation. | ||
* Prefer {@link Optional#orElse(Object)} over {@link Optional#orElseGet(Supplier)} if the | ||
* fallback value does not require non-trivial computation. | ||
*/ | ||
// XXX: This rule may introduce a compilation error: the `value` expression may reference a | ||
// non-effectively final variable, which is not allowed in the replacement lambda expression. | ||
// Review whether a `@Matcher` can be used to avoid this. | ||
// XXX: Once `MethodReferenceUsage` is "production ready", replace | ||
// `@NotMatches(IsLikelyTrivialComputation.class)` with `@Matches(RequiresComputation.class)` (and | ||
// reimplement the matcher accordingly). | ||
static final class OptionalOrElseGet<T> { | ||
// XXX: This rule is the counterpart to the `OptionalOrElseGet` bug checker. Once the | ||
// `MethodReferenceUsage` bug checker is "production ready", that bug checker may similarly be | ||
// replaced with a Refaster rule. | ||
static final class OptionalOrElse<T> { | ||
@BeforeTemplate | ||
T before(Optional<T> optional, @NotMatches(IsLikelyTrivialComputation.class) T value) { | ||
return optional.orElse(value); | ||
T before(Optional<T> optional, @NotMatches(RequiresComputation.class) T value) { | ||
return optional.orElseGet(() -> value); | ||
} | ||
|
||
@AfterTemplate | ||
T after(Optional<T> optional, T value) { | ||
return optional.orElseGet(() -> value); | ||
return optional.orElse(value); | ||
} | ||
} | ||
Comment on lines
257
to
274
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rule replaced: the old definition is absorbed into the bug checker, while the new rule implements complementary logic. |
||
|
||
|
@@ -373,7 +370,12 @@ Optional<R> after(Optional<T> optional, Function<? super S, Optional<? extends R | |
/** Prefer {@link Optional#or(Supplier)} over more verbose alternatives. */ | ||
static final class OptionalOrOtherOptional<T> { | ||
@BeforeTemplate | ||
@SuppressWarnings("NestedOptionals") | ||
@SuppressWarnings({ | ||
"LexicographicalAnnotationAttributeListing" /* `key-*` entry must remain last. */, | ||
"NestedOptionals" /* This violation will be rewritten. */, | ||
"OptionalOrElse" /* Parameters represent expressions that may require computation. */, | ||
"key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict" | ||
}) | ||
Optional<T> before(Optional<T> optional1, Optional<T> optional2) { | ||
// XXX: Note that rewriting the first and third variant will change the code's behavior if | ||
// `optional2` has side-effects. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,14 +5,15 @@ | |
import com.google.errorprone.CompilationTestHelper; | ||
import org.junit.jupiter.api.Test; | ||
|
||
final class OptionalOrElseTest { | ||
final class OptionalOrElseGetTest { | ||
@Test | ||
void identification() { | ||
CompilationTestHelper.newInstance(OptionalOrElse.class, getClass()) | ||
CompilationTestHelper.newInstance(OptionalOrElseGet.class, getClass()) | ||
.addSourceLines( | ||
"A.java", | ||
"import com.google.errorprone.refaster.Refaster;", | ||
"import java.util.Optional;", | ||
"import java.util.function.Supplier;", | ||
"", | ||
"class A {", | ||
" private final Optional<Object> optional = Optional.empty();", | ||
|
@@ -27,6 +28,7 @@ void identification() { | |
" optional.orElse(string);", | ||
" optional.orElse(this.string);", | ||
" optional.orElse(Refaster.anyOf(\"constant\", \"another\"));", | ||
" Optional.<Supplier<String>>empty().orElse(() -> \"constant\");", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New test for the case that triggered this PR, observed with Picnic-internal code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clearer: previously the bug checker would not see the lambda expression as a trivial computation, while the Refaster rule would. In this context the latter was right. |
||
"", | ||
" // BUG: Diagnostic contains:", | ||
" Optional.empty().orElse(string + \"constant\");", | ||
|
@@ -67,7 +69,7 @@ void identification() { | |
|
||
@Test | ||
void replacement() { | ||
BugCheckerRefactoringTestHelper.newInstance(OptionalOrElse.class, getClass()) | ||
BugCheckerRefactoringTestHelper.newInstance(OptionalOrElseGet.class, getClass()) | ||
.addInputLines( | ||
"A.java", | ||
"import java.util.Optional;", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This observation was only moved from the Refaster rule to here. Fixing it is out of scope.