-
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
Rewrite Optional#orElse
to Optional#orElseGet
for non-constant expressions
#527
Conversation
Looks good. No mutations were possible for these changes. |
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.
Clearly we need to do a bit more work here. To complicate matters further, this rule will introduce quite a lot of lambda expressions that we'd like to write as a method reference instead. So I wonder whether we should first finalize the (very hard) task of getting MethodReferenceUsage
production ready. (I have a branch for that, but realistically it'll be months (or longer...) before I'll get back to that).
Added a small commit.
* Prefer {@link Optional#orElseGet(Supplier)} over {@link Optional#orElse(Object)} if the given | ||
* value is not a compile-time constant. | ||
*/ | ||
abstract static class OrElseToOrElseGet<T> { |
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.
abstract static class OrElseToOrElseGet<T> { | |
static final class OrElseToOrElseGet<T> { |
(This one is also suggested when running ./apply-error-prone-suggestions.sh
.)
*/ | ||
abstract static class OrElseToOrElseGet<T> { | ||
@BeforeTemplate | ||
T before(Optional<T> o, @NotMatches(CompileTimeConstantExpressionMatcher.class) T value) { |
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.
So it seems that perhaps CompileTimeConstantExpressionMatcher
is a bit too strict. When we run apply-error-prone-suggestions.sh
this diff is also generated:
diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetectionTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetectionTest.java
index 70282dd4..5b430448 100644
--- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetectionTest.java
+++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetectionTest.java
@@ -67,7 +67,7 @@ final class ConflictDetectionTest {
return ConflictDetection.findMethodRenameBlocker(
ASTHelpers.getSymbol(tree), tree.getName() + "t", state)
.map(blocker -> buildDescription(tree).setMessage(blocker).build())
- .orElse(Description.NO_MATCH);
+ .orElseGet(() -> Description.NO_MATCH);
}
}
}
Similarly, as-is the rule triggers the following violation:
[WARN] /home/sschroevers/workspace/picnic/error-prone-support/refaster-support/src/main/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformer.java:[124,80] [Refaster Rule] OptionalRules.OrElseToOrElseGet: Refactoring opportunity
(see https://error-prone.picnic.tech/refasterrules/OptionalRules#OrElseToOrElseGet)
Did you mean 'return getAnnotationValue(Severity.class, Severity::value, delegate).orElseGet(()->SUGGESTION);'?
We might need a separate Matcher
that allows any constant and field dereference 🤔
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.
Agreed, after running the script I did notice a lot of changes and some seemed unintended, in the end we're looking to prevent potentially expensive method invocations used with orElse
, perhaps it would be a better to match based on method invocations instead?
* Prefer {@link Optional#orElseGet(Supplier)} over {@link Optional#orElse(Object)} if the given | ||
* value is not a compile-time constant. |
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.
I'll need to think a bit about phrasing, but likely we should call out that this rule can, in rare circumstances, change application behavior. (This can happen if value
matches a non-pure expression.)
Looks good. No mutations were possible for these changes. |
Hi @mlrprananta , I discussed this with @Stephan202 offline and we thought of an approach and are curious to hear what you think.
This is a blocker for enabling this rule internally. The
WDYT? 😄 |
Hey @rickie, between those 2 options I would prefer the second route for now, as I don't feel comfortable not enabling it internally. Second route would be a easier to cover as well initially and as you mentioned we can improve it later on! |
@mlrprananta sounds good to me! Let's go for that option 😄. |
be70407
to
0709a49
Compare
Looks good. All 7 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
1 similar comment
Looks good. All 7 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
error-prone-contrib/src/main/java/tech/picnic/errorprone/refasterrules/OptionalRules.java
Outdated
Show resolved
Hide resolved
...t/java/tech/picnic/errorprone/refaster/matchers/IsMethodInvocationWithTwoOrMoreArgsTest.java
Outdated
Show resolved
Hide resolved
return optional.map(predicate).orElse(Refaster.anyOf(false, Boolean.FALSE)); | ||
return Refaster.anyOf( | ||
optional.map(predicate).orElse(false), optional.map(predicate).orElse(Boolean.FALSE)); |
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.
Had to rewrite this as this was causing a "false" positive, orElse(Refaster.anyOf(false, Boolean.FALSE))
got rewritten to orElseGet(() -> Refaster.anyOf(false, Boolean.FALSE))
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.
Doubting a little between this and adding a suppression 🤔. I'm leaning towards a suppression as this might raise questions, but let's see what @Stephan202 thinks.
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.
Ah, ideally the new matcher ignores Refaster.anyOf
calls, since those aren't true method invocations.
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.
... but the effort of doing this and properly testing it may not be worth it, so let's go with a suppression :)
Looks good. All 7 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
1 similar comment
Looks good. All 7 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
229a3c0
to
0b73b98
Compare
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.
Rebased and added a commit. There is the generalization of the Matcher left to also account for multiple method invocations :).
Cool stuff 🚀 !
return optional.map(predicate).orElse(Refaster.anyOf(false, Boolean.FALSE)); | ||
return Refaster.anyOf( | ||
optional.map(predicate).orElse(false), optional.map(predicate).orElse(Boolean.FALSE)); |
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.
Doubting a little between this and adding a suppression 🤔. I'm leaning towards a suppression as this might raise questions, but let's see what @Stephan202 thinks.
// XXX: Extend rule to all method invocations (with less than 2 arguments). | ||
static final class OptionalOrElseGet<T> { | ||
@BeforeTemplate | ||
T before(Optional<T> o, @Matches(IsMethodInvocationWithTwoOrMoreArgs.class) T value) { |
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.
T before(Optional<T> o, @Matches(IsMethodInvocationWithTwoOrMoreArgs.class) T value) { | |
T before(Optional<T> optional, @Matches(IsMethodInvocationWithTwoOrMoreArgs.class) T value) { |
This is used more often in the file, so lets change.
@Test | ||
void matches() { | ||
CompilationTestHelper.newInstance( | ||
IsMethodInvocationWithTwoOrMoreArgsTest.MatcherTestChecker.class, getClass()) |
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.
IsMethodInvocationWithTwoOrMoreArgsTest.MatcherTestChecker.class, getClass()) | |
MatcherTestChecker.class, getClass()) |
We can drop the qualifier.
private static final long serialVersionUID = 1L; | ||
|
||
/** Instantiates a new {@link IsMethodInvocationWithTwoOrMoreArgs} instance. */ | ||
public IsMethodInvocationWithTwoOrMoreArgs() {} |
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.
Interesting, here we discussed that we want to check for two MethodInvocations instead of one method invocation wit two arguments. This is not incorrect though. I think it is nice to combine the two things into one Matcher
and call it something like: IsChainedMethodCallOrHasAtLeastTwoArgs
. I tried implementing this but had some troubles with the VisitorState
in the matches
method.
In the AbstractMatcherTestChecker
we only pass the VisitorState
from the CompilationUnit
which is not the state that we want. So we need to probably change that setup a bit. For now I'll flush the changes that I have.
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.
Interesting, here we discussed that we want to check for two MethodInvocations instead of one method invocation wit two arguments.
Ah, that was a misunderstanding on my part, but indeed both could apply for this situation. I can look into it a bit more to implement a (combined) matcher for chained method calls as well
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.
Hey @rickie, few questions:
- Could you elaborate on the
VisitorState
issue you mentioned? Which state should we be using instead? - Could nested method invocations also apply for this situation? So something like:
m1(m2("param"))
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.
Indeed, the matcher shouldn't require >= 2 arguments: foo().bar()
doesn't have any arguments, yet should be matched. One option is to use a TreeScanner
(see examples in the code base) to count the number of MethodInvocationTree
s.
IIUC the idea is to match non-trivial expressions that can't (or shouldn't) be replaced with a method reference. In this case we know that we're rewriting the expression to a Supplier<T>
. So perhaps we're looking for a matcher of expressions which:
- Contain more than one method invocation; or
- Contain at least one method invocation with one or more arguments.
(Invocations of a single nullary method such as foo()
, x.foo()
or x.y.foo()
should not be matched for now, since in those case a method reference may be preferable.)
That said, expressions of the form 42 * foo()
should also be deferred.... So perhaps we should instead introduce an IsLikelyTrivialComputation
matcher that does match true
, x
, foo()
, x.foo()
, x.y.foo()
etc., and then have the Refaster rule use @NotMatches(IsLikelyTrivialComputation.class)
. Then later once MethodReferenceUsage
works properly, we can replace this matcher with a simpler RequiresComputation
matcher and then use @Matches(RequiresComputation.class)
.
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.
In the
AbstractMatcherTestChecker
we only pass theVisitorState
from theCompilationUnit
which is not the state that we want. So we need to probably change that setup a bit. For now I'll flush the changes that I have.
I have a fix for this on a branch somewhere; let me try to open a PR for this ~soon.
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.
I filed #599. As stated there, my suggestion would be to rebase this branch on top of that branch.
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.
@Stephan202 Had some time to look at this again, and realized that I did not have a clear look at this before 😅
Indeed we're trying to only convert non-trivial expressions into Supplier<T>
, and avoiding expressions that could be converted into a method reference.
I like the idea of the IsLikelyTrivialComputation
matcher which would match trivial cases + method reference cases. However, IIUC, any expression that ends with a nullary method could in this case be converted into a method reference (even if it has 2 or more method invocations), e.g:
x() -> this::x
x().y() -> x()::y
x.y() -> x::y
x.y.z() -> x.y::z
x.y().z() -> x.y()::z
x.y("x").z() -> x.y("x")::z
etc.
This would mean the matcher should match any expressions that ends the chain with a nullary method invocation, right?
However I do see that IntelliJ mentions semantics might be changed when converting these "chain" expressions into method references, and perhaps it was already decided beforehand that we will not cover these cases for the method reference bug checker?
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.
Nice examples @mlrprananta !
I checked in IntelliJ and saw that it mentions this optimization once you press alt+enter
on the specific method. However, it doesn't highlight the method invocation to indicate you should rewrite it to a method reference, which for simple cases IntelliJ does. Because for the more difficult ones it doesn't do that, I would say it is fine for now to consider these things as a non trivial computation and we can rewrite them. Probably the last three examples will not occur that often.
So I think the fist three examples are on the "edge" of being a "trivial" computation... I'm not sure about those. The others can definitely be rewritten in the context of this PR.
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.
Discussed this with @Stephan202 offline;
We can stick to our original plan, the IsChainedMethodCallOrHasAtLeastTwoArgs
matcher (name could be improved maybe) to flag two or more method invocations or when an invocation has two arguments.
This would flag the occurrences shown in the examples for which it is a little awkward to rewrite them to method references.
A problem is that there are many edge cases and questions that we can raise about the "boundary" of a IsLikelyTrivialComputation
Matcher as you can go quite far with the things that you do or don't want to flag. Making it that difficult is probably not worth the time. Additionally it's less worth the effort as we should finish the MethodReferenceUsage
check at some point anyways.
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.
Alright, so I had a close look at this, and pushed a proposal 🙃
"", | ||
" String positive2() {", | ||
" // BUG: Diagnostic contains:", | ||
" return String.format(\"%s\", \"foo\");", |
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.
We use the word "or more" for the Matcher, so we should add a test case :).
Will push some tweaks to simplify a little.
.addSourceLines( | ||
"A.java", | ||
"class A {", | ||
" String foo(String foo, String bar) {", |
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.
Let's move these to the bottom :).
|
||
ImmutableSet<String> testOptionalOrElseGet() { | ||
return ImmutableSet.of( | ||
Optional.of("foo").orElse("bar"), |
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.
We make sure to use distinct values for every statement :).
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.
Does it need to be distinct for mutation testing?
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.
Nope, it's about making sure that Refaster properly moves things around (which is easier to see if all values are distinct). Unfortunately for technical reasons our Refaster tests aren't exercised by Pitest.
5084a81
to
86c429c
Compare
Looks good. All 4 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
1 similar comment
Looks good. All 4 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
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.
Interesting discussions!
return optional.map(predicate).orElse(Refaster.anyOf(false, Boolean.FALSE)); | ||
return Refaster.anyOf( | ||
optional.map(predicate).orElse(false), optional.map(predicate).orElse(Boolean.FALSE)); |
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.
Ah, ideally the new matcher ignores Refaster.anyOf
calls, since those aren't true method invocations.
private static final long serialVersionUID = 1L; | ||
|
||
/** Instantiates a new {@link IsMethodInvocationWithTwoOrMoreArgs} instance. */ | ||
public IsMethodInvocationWithTwoOrMoreArgs() {} |
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.
Indeed, the matcher shouldn't require >= 2 arguments: foo().bar()
doesn't have any arguments, yet should be matched. One option is to use a TreeScanner
(see examples in the code base) to count the number of MethodInvocationTree
s.
IIUC the idea is to match non-trivial expressions that can't (or shouldn't) be replaced with a method reference. In this case we know that we're rewriting the expression to a Supplier<T>
. So perhaps we're looking for a matcher of expressions which:
- Contain more than one method invocation; or
- Contain at least one method invocation with one or more arguments.
(Invocations of a single nullary method such as foo()
, x.foo()
or x.y.foo()
should not be matched for now, since in those case a method reference may be preferable.)
That said, expressions of the form 42 * foo()
should also be deferred.... So perhaps we should instead introduce an IsLikelyTrivialComputation
matcher that does match true
, x
, foo()
, x.foo()
, x.y.foo()
etc., and then have the Refaster rule use @NotMatches(IsLikelyTrivialComputation.class)
. Then later once MethodReferenceUsage
works properly, we can replace this matcher with a simpler RequiresComputation
matcher and then use @Matches(RequiresComputation.class)
.
private static final long serialVersionUID = 1L; | ||
|
||
/** Instantiates a new {@link IsMethodInvocationWithTwoOrMoreArgs} instance. */ | ||
public IsMethodInvocationWithTwoOrMoreArgs() {} |
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.
In the
AbstractMatcherTestChecker
we only pass theVisitorState
from theCompilationUnit
which is not the state that we want. So we need to probably change that setup a bit. For now I'll flush the changes that I have.
I have a fix for this on a branch somewhere; let me try to open a PR for this ~soon.
|
||
ImmutableSet<String> testOptionalOrElseGet() { | ||
return ImmutableSet.of( | ||
Optional.of("foo").orElse("bar"), |
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.
Nope, it's about making sure that Refaster properly moves things around (which is easier to see if all values are distinct). Unfortunately for technical reasons our Refaster tests aren't exercised by Pitest.
Nice answers @Stephan202 😄, curious to see the PR 🚀 ! |
86c429c
to
d61816d
Compare
I changed the base to #599 already 😄. |
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.
Added a commit; PTAL :)
(I'd be nice to get some reviews of the base PR 🙃)
return optional.map(predicate).orElse(Refaster.anyOf(false, Boolean.FALSE)); | ||
return Refaster.anyOf( | ||
optional.map(predicate).orElse(false), optional.map(predicate).orElse(Boolean.FALSE)); |
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.
... but the effort of doing this and properly testing it may not be worth it, so let's go with a suppression :)
Optional.of("foo").orElse("bar"), | ||
Optional.of("baz").orElse(toString()), | ||
Optional.of("qux").orElse(String.valueOf(true)), | ||
Optional.of("quux").orElse(String.format("%s", "quuz")), | ||
Optional.of("corge").orElse(String.format("%s", "grault", "garply"))); |
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.
I guess we can do with fewer test cases :)
private static final long serialVersionUID = 1L; | ||
|
||
/** Instantiates a new {@link IsMethodInvocationWithTwoOrMoreArgs} instance. */ | ||
public IsMethodInvocationWithTwoOrMoreArgs() {} |
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.
Alright, so I had a close look at this, and pushed a proposal 🙃
Looks good. All 45 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
1d630d7
to
386fd74
Compare
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.
One consideration, we already discussed that we don't want too many edge cases for this Matcher, so maybe it's already fine like this.
Anyway, really nice improvement! Curious to see how this performs on our codebase. 🚀 !
" }", | ||
"", | ||
" int negative6() {", | ||
" return 1 * 2;", |
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.
For the BinaryTree
we can check if both left and right are an LiteralTree
or IdentifierTree
as start? Also fine with leaving this as is.
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.
Yeah, one could argue that this is a trivial computation. But then maybe so is 4 * 60 * 60
("four hours"), which still wouldn't be flagged. And since the matcher is already quite complex, I'm not sure how far to go for diminishing returns. Even this case I don't expect to be triggered too frequently (at least in our internal code base).
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.
Tnx for the review @rickie. @mlrprananta we need one more review of #599 before we can merge this one. Are you up for that?
" }", | ||
"", | ||
" int negative6() {", | ||
" return 1 * 2;", |
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.
Yeah, one could argue that this is a trivial computation. But then maybe so is 4 * 60 * 60
("four hours"), which still wouldn't be flagged. And since the matcher is already quite complex, I'm not sure how far to go for diminishing returns. Even this case I don't expect to be triggered too frequently (at least in our internal code base).
a127136
to
ead55db
Compare
Fair point --> Rebased --> Will merge. |
Looks good. All 45 mutations in this change were killed.
Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed. |
Kudos, SonarCloud Quality Gate passed! |
Updated the suggested commit message. |
The second like seems now less important than perhaps it is, but I lack creativity for a better proposal. Let's go 🚀 |
Yeah you are right, I tried combining them but the name is too long 😅. |
❗
This is now on top of #599❗Following an internal GH thread it felt fitting to write a rule to cover this case.
This PR proposes a Refaster rule to rewrite
Optional#orElse
toOptional#orElseGet
for method invocation expressions, (with 2 or more args), with performance as the main benefit.