Skip to content
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

[improvement] Ensure Optional#orElse argument is not method invocation #655

Merged
merged 7 commits into from
Jun 21, 2019
Merged

[improvement] Ensure Optional#orElse argument is not method invocation #655

merged 7 commits into from
Jun 21, 2019

Conversation

gatesn
Copy link
Contributor

@gatesn gatesn commented Jun 19, 2019

Before this PR

Run into Optional#orElse code three times now that was intended to be lazy.

After this PR

Argument to Optional#orElse must not include a method invocation. Otherwise a fix is suggested to convert to Optional#orElseGet(Supplier)

@gatesn gatesn requested a review from a team as a code owner June 19, 2019 20:00
@markelliot
Copy link
Contributor

I think compile time constant is too aggressive. We should just enforce it’s not an expression (a variable further up the function is totally appropriate as an argument to orElse).

@gatesn gatesn changed the title [improvement] Ensure Optional#orElse argument is compile-time constant [improvement] Ensure Optional#orElse argument is not method invocation Jun 19, 2019
link = "https://github.com/palantir/gradle-baseline#baseline-error-prone-checks",
linkType = BugPattern.LinkType.CUSTOM,
severity = SeverityLevel.ERROR,
summary = "Allow passing only compile-time constants to Optional#orElse.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update this as well?


@AutoService(BugChecker.class)
@BugPattern(
name = "OptionalOrElseConstant",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a little stale, perhaps OptionalOrElseMethodInvocation, OptionalOrElseValue?

.setMessage("Optional#orElse uses a non-constant expression")
.addFix(SuggestedFix.builder()
.postfixWith(tree.getMethodSelect(), "Get")
.prefixWith(orElseArg, "() -> ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pass a method reference instead of allocating a new lamba? i.e. .orElseGet(this::foo) or orElseGet(foo)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not so easy to detect these things, and if we could, they should probably be done as their own check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

link = "https://github.com/palantir/gradle-baseline#baseline-error-prone-checks",
linkType = BugPattern.LinkType.CUSTOM,
severity = SeverityLevel.ERROR,
summary = "Ensure Optional#orElse argument does not invoke a ny methods.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/a ny/any

It's fairly obvious in this case, but it may be helpful to spell out exactly why orElseGet is better in the summary

@carterkozak
Copy link
Contributor

Very excited about this check. Thanks for implementing the suggested fix, they've been invaluable for a certain large internal project!

@gatesn
Copy link
Contributor Author

gatesn commented Jun 20, 2019

Addressed all comments, should be good to go

Copy link
Contributor

@markelliot markelliot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test that shows we prevent foo + bar as an argument to orElse?

@iamdanfox
Copy link
Contributor

Probably worth mentioning this on the README with the other error-prone checks too pls! https://github.com/palantir/gradle-baseline#baseline-error-prone-checks

" Optional.of(\"hello\").orElse(\"constant\");",
" Optional.of(\"hello\").orElse(compileTimeConstant);",
" String string = f();",
" Optional.of(\"hello\").orElse(string);",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is missing test coverage for Optional.of(foo).orElseGet(() -> bar);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean, you want this check to revert it to orElse(bar)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a test verifying that Optional.of(foo).orElseGet(() -> bar) is an acceptable thing to write

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I mean Optional.of(foo).orElseGet(() -> f());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I've done that now

"import java.util.Optional;",
"class Test {",
" String f() { return \"hello\"; }",
" // BUG: Diagnostic contains: non-constant expression",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "non-constant" mean? suspect that's not quite the right word. what you mean here is "method invocation"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it is, but then as Mark says this doesn't cover operator expressions. Not sure what the right word is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the Matchers.contains(ExpressionTree.class, MethodMatchers.anyMethod()) thing correctly, then it errors iff a method is called anywhere inside the orElse(...). A reasonable description would be, for instance, Expression passed to Optional#orElse must not invoke a method, or positively (not sure what the right one is), Expression passed to Optional#orElse invokes a method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or contains a method invocation, if you prefer). To double-check: foo.orElse(bar + baz) is allowed, ja?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it is currently but should it be allowed? Are operators ever sufficiently expensive for us to care about? If yes, then we need to think again about our matcher. Perhaps compileTimeConstant OR identifier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String concatenation using operators should be required to execute in a lambda. I don't mind the lambda allocation for math because the optional is already boxing numerical results with similar (avoidable) overhead to creating a lambda.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. then you'll likely see

String bar = boom + baz;
return foo.orElse(bar);  // can't concat in here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really mind the exact version of the check, as long as you make the text reflect the check. (that's what I was commenting on initially, sorry if this was unclear.)

@gatesn
Copy link
Contributor Author

gatesn commented Jun 21, 2019

Is this good to go now?

Copy link
Contributor

@markelliot markelliot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to consider in a separate PR the opposite of this — if you’re calling orElseGet when you could be calling orElse it’d be good to warn (IntelliJ has that as a refactor suggestion), but not worth blocking on

@gatesn gatesn merged commit f1264dd into palantir:develop Jun 21, 2019
@gatesn gatesn deleted the ngates/constant-or-else-expr branch June 21, 2019 09:54
@iamdanfox
Copy link
Contributor

Didn't update the README 😢

@ferozco
Copy link
Contributor

ferozco commented Jun 21, 2019

@gatesn I think that this check may be overly aggressive since accessing any field on an immutable is considered a method invocation. In general, I feel like this check has a very large number of false positives so we may want to lower the severity to warn or reconsider the check entirely.

@gatesn
Copy link
Contributor Author

gatesn commented Jun 21, 2019

@ferozco can you / @carterkozak enumerate the false positives in case we want to pick this back up?

There are a few I disagree with, for instance I'm not convinced there's a difference in accessing a property of an immutable as orElse(foo.bar()) or orElseGet(foo::bar). If its purely style, I'd strongly suggest we prefer the latter.

A viable opposition would be for example Collections.emptyList(). However it would be useful to understand all the cases you hit so we can attempt to refine the check.

@markelliot
Copy link
Contributor

It might be that we need an automated refactor of method calls into suppliers in order to make this work. Seems reasonable that you'd orElseGet(Collections::emptyList), too.

@gatesn
Copy link
Contributor Author

gatesn commented Jun 21, 2019

If that's the case, then I imagine it shouldn't be too hard to implement the logic as either in the check itself, or as part of a refaster rule

@gatesn
Copy link
Contributor Author

gatesn commented Jun 24, 2019

So, we can now auto-apply error-prone fixes: https://github.com/palantir/gradle-baseline/pull/660/files

Is this sufficient? Or do we want to block on having the () -> foo() -> this::foo rewrite

David132639 added a commit to David132639/gradle-baseline that referenced this pull request Mar 29, 2023
Add a line for an error-prone check added in palantir#655, which is missing from the README.
bulldozer-bot bot pushed a commit that referenced this pull request Apr 4, 2023
Add a line for an error-prone check added in #655, which is missing from the README.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants