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

New InstanceOfPatternMatch recipe #2690

Merged
merged 1 commit into from
Jan 31, 2023

Conversation

knutwannheden
Copy link
Contributor

Implement a recipe which for Java 17+ transforms instanceof expressions which have a corresponding type cast within their flow scope (see https://openjdk.org/jeps/394 for details) to an instanceof with a pattern variable and then also substitutes the type cast expressions accordingly.

@knutwannheden
Copy link
Contributor Author

This is still a draft. There are still a few TODOs and the code is probably not very OpenRewrite-idiomatic, as I probably missed out on some helper utilities or misunderstood how to use all available constructs. A critical review would be most appreciated!

@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch from ca849eb to 6627fd9 Compare January 23, 2023 11:00
@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch from 6627fd9 to 73f101d Compare January 23, 2023 11:18
@timtebeek timtebeek added the recipe Requested Recipe label Jan 23, 2023
@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch 4 times, most recently from bafd93a to 595be0a Compare January 24, 2023 13:53
@knutwannheden knutwannheden marked this pull request as ready for review January 24, 2023 13:53
@knutwannheden
Copy link
Contributor Author

knutwannheden commented Jan 24, 2023

An interesting enhancement could be to inline the expression, if it isn't referenced anywhere else. E.g.

Object o = foo.bar();
if (o instanceof String && ((String) o).isEmpty()) {
    // ...
}

would become:

if (foo.bar() instanceof String o && o.isEmpty()) {
    // ...
}

if o isn't referenced anywhere outside the if-statement. I would however propose to file a separate enhancement request for this and maybe that should even be a completely independent (reusable) visitor.

Note that since Java isn't a lazily evaluated language, this "optimization" is only possible when the variable declaration immediately precedes the if-statement or there are no statements in between which could have side-effects (or the expression itself is known to be free of side-effects).

@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch 3 times, most recently from d892aea to 7484889 Compare January 25, 2023 09:29
@knutwannheden
Copy link
Contributor Author

knutwannheden commented Jan 25, 2023

@sambsnyd I consider this PR to be ready to be reviewed now. I still think that some things may be a bit non-idiomatic and would be glad for some tips on how to improve the code. (I am for instance wondering if it would be more idiomatic to schedule a separate after-recipe for each occurrence, rather than try to collect them all into a single input for the visitor or possibly it is even considered better practice to directly "inline" the visitor into the respective visitXyz() method and call the visit(Tree, P, Cursor) method on it.)

To be on the safe side, I currently exclude any expressions, which may have side effects (as determined by Expression#getSideEffects()). I noticed that a few expression types were missing the implementation for this method or had an incorrect implementation. I will look into this in a separate PR. (I would also like to consider support for IDEA's @Contract(pure=true) and possibly also the Checker framework's corresponding annotation, but also that will be a separate issue.)

Copy link
Member

@sambsnyd sambsnyd left a comment

Choose a reason for hiding this comment

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

Nice work. The recipe is complex, but the operation being performed is complex, so I don't see an obvious way to simplify things.

@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch from 7484889 to e1d1f9d Compare January 25, 2023 20:22
@sambsnyd
Copy link
Member

@sambsnyd I consider this PR to be ready to be reviewed now. I still think that some things may be a bit non-idiomatic and would be glad for some tips on how to improve the code. (I am for instance wondering if it would be more idiomatic to schedule a separate after-recipe for each occurrence, rather than try to collect them all into a single input for the visitor or possibly it is even considered better practice to directly "inline" the visitor into the respective visitXyz() method and call the visit(Tree, P, Cursor) method on it.)

Collecting them all into a single input for the visitor is definitely better for performance than separate after-recipe for each. Better to pay the cost of traversing the LST once rather than n times.
I have developed a preference for visitXyz() rather than doNext(). I often find it easier to debug and reason about the state of the LST when the change I'm looking to effect is made immediately. There is nothing wrong with using doNext(), and it is particularly convenient when the change you are trying to effect is going to happen elsewhere in the tree.

To be on the safe side, I currently exclude any expressions, which may have side effects (as determined by Expression#getSideEffects()). I noticed that a few expression types were missing the implementation for this method or had an incorrect implementation. I will look into this in a separate PR. (I would also like to consider support for IDEA's @Contract(pure=true) and possibly also the Checker framework's corresponding annotation, but also that will be a separate issue.)

Those sound like good improvements

@knutwannheden
Copy link
Contributor Author

Nice work. The recipe is complex, but the operation being performed is complex, so I don't see an obvious way to simplify things.

I did figure out a way to slightly simplify things. Since I already used the cursor for the containing statement to "communicate" between the instanceof and the type cast expressions, I now added a postVisit() method which simply checks for the corresponding message in the cursor and if it finds a non-empty result, it means that there is a replacement to be performed and it can post-process the statement using the other visitor. This way the second visitor only needs to process small sub-trees rather than the whole source again.

I was looking for a mechanism to instruct a cursor to perform some post-processing action, once the visitor leaves that scope. As it turns out the postVisit() method can be used for this purpose when adding a message to the corresponding cursor.

@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch 3 times, most recently from 56d6649 to 4c37972 Compare January 25, 2023 21:23
@knutwannheden
Copy link
Contributor Author

@sambsnyd Thanks for your review feedback! I did find and fix one more bug and added a few more tests. There are probably more corner cases which should be tested, but I can't think of them right now.

@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch from 4c37972 to 92b9dde Compare January 25, 2023 22:22
Implement a recipe which for Java 17+ transforms `instanceof` expressions which have a corresponding type cast within their flow scope (see https://openjdk.org/jeps/394 for details) to an `instanceof` with a pattern variable and then also substitutes the type cast expressions accordingly.

Currently, this works inside any binary boolean expressions and the then-block of `if` statements and ternary operator expressions.
@knutwannheden knutwannheden force-pushed the instanceof-pattern-recipe branch from 92b9dde to 01b2070 Compare January 26, 2023 10:14
@timtebeek
Copy link
Contributor

Over on this issue the question came up if Java 9+ recipes should go into rewrite-migrate-java rather than openrewrite/rewrite.

Intuitively I'd think any "new"-ish Java features would best fit into org.openrewrite.java.migrate, and be aggregated in that module in yaml migration recipes to complete a migration. That way there's also no question what Java source level is required for openrewrite/rewrite: 8 (for now), to best fit with what we're seeing in the wild.

What are your thoughts on that @sambsnyd ?

@sambsnyd
Copy link
Member

@timtebeek I see rewrite-migrate-java as being for recipes about migrating to new versions of java. I don't think we need to put all recipes applicable only to newer versions of java there. This feels like a cleanup recipe more than a migration recipe, so putting it into rewrite-java feels reasonable to me

@sambsnyd sambsnyd merged commit 5709e54 into openrewrite:main Jan 31, 2023
@knutwannheden knutwannheden deleted the instanceof-pattern-recipe branch January 31, 2023 08:49
@knutwannheden
Copy link
Contributor Author

This feels like a cleanup recipe more than a migration recipe, so putting it into rewrite-java feels reasonable to me

@sambsnyd Should the recipe be moved to the cleanup subpackage?

knutwannheden added a commit to knutwannheden/rewrite-migrate-java that referenced this pull request Jan 31, 2023
sambsnyd pushed a commit to openrewrite/rewrite-migrate-java that referenced this pull request Jan 31, 2023
* Add `InstanceOfPatternMatch` recipe to `java-version-17.yml`

Related: openrewrite/rewrite#2690

* Move InstanceOfPatternMatch under UpgradeToJava17

* Add test for `InstanceOfPatternMatch` recipe

---------

Co-authored-by: Tim te Beek <tim@moderne.io>
knutwannheden added a commit that referenced this pull request Jan 31, 2023
@sambsnyd sambsnyd added this to the 7.36.0 milestone Feb 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
recipe Requested Recipe
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

3 participants