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

Fix array annotations #3464

Closed

Conversation

sfc-gh-dtran
Copy link

@sfc-gh-dtran sfc-gh-dtran commented Aug 7, 2023

What's changed?

Fixes parsing for annotated array types

What's your motivation?

Fixes #3453
Fixes #2911
Fixes #3440
Fixes #3667

Anything in particular you'd like reviewers to focus on?

I add a new J.ArrayType.AnnotatedDimension auxiliary structure to store everything needed to represent a single dimension (e.g. <annotations> [])

Anyone you would like to review specifically?

@timtebeek @kunli2

Additional context

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've added the license header to any new files through ./gradlew licenseFormat
  • I've used the IntelliJ auto-formatter on affected files
  • I've updated the documentation (if applicable)

@timtebeek timtebeek added bug Something isn't working enhancement New feature or request labels Aug 7, 2023
Comment on lines +80 to +88
int[][] ints = new int[2][2];

Integer[] @Nonnull [] integers = new Integer[1][1];

Integer @Nonnull [] foo = new Integer[1];

public int @Nonnull [] @Nonnull [] getInts() {
return ints;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks a lot for being thorough with the various forms of annotations and whitespace here! Good to have this covered.

while(classNode != null && classNode.isArray()) {
classNode = classNode.getComponentType();
result.add(JRightPadded.build(sourceBefore("[")).withAfter(sourceBefore("]")));

// TODO: add support for getting each dimensions' annotations from classNode
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we associate an issue with this TODO item and link it here such that it's not lost?
And what's the impact of not yet covering this in the current implementation?

Copy link
Author

@sfc-gh-dtran sfc-gh-dtran Aug 7, 2023

Choose a reason for hiding this comment

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

The impact of not covering this should just be that the same bug will appear in Groovy annotated arrays. I'm not as familiar with the Groovy language specification (I actually don't even know if this annotated array syntax is valid in Groovy), and the GroovyParserVisitor is written differently to the JavaParserVisitors, so I opted to not implement it for now. It should be very similar to the Java case though; the AnnotatedDimension structure is already there so it should be easy to implement.

So yes, an issue for this TODO would probably be good

@timtebeek
Copy link
Contributor

Thanks a lot for this effort @sfc-gh-dtran , and your clear communication around the issue and your approach. Due to the addition of ArrayType.AnnotatedDimension I'm going to defer a more thorough review to Kun & Sam, as they are more experienced around the parsers and impact of adding new constructs on for instance (de)serialization.

From what I read in Slack you're up and running with this change locally; does that mean you're unblocked for making changes ? I know you indicated you needed proper support here, and appreciate you stepping up. I just want to make sure you're on your way before we get to a review, merge & release.

@sfc-gh-dtran
Copy link
Author

Thanks a lot for this effort @sfc-gh-dtran , and your clear communication around the issue and your approach. Due to the addition of ArrayType.AnnotatedDimension I'm going to defer a more thorough review to Kun & Sam, as they are more experienced around the parsers and impact of adding new constructs on for instance (de)serialization.

From what I read in Slack you're up and running with this change locally; does that mean you're unblocked for making changes ? I know you indicated you needed proper support here, and appreciate you stepping up. I just want to make sure you're on your way before we get to a review, merge & release.

Thanks a lot for this effort @sfc-gh-dtran , and your clear communication around the issue and your approach. Due to the addition of ArrayType.AnnotatedDimension I'm going to defer a more thorough review to Kun & Sam, as they are more experienced around the parsers and impact of adding new constructs on for instance (de)serialization.

From what I read in Slack you're up and running with this change locally; does that mean you're unblocked for making changes ? I know you indicated you needed proper support here, and appreciate you stepping up. I just want to make sure you're on your way before we get to a review, merge & release.

Yes! I'm unblocked for making changes.

Copy link
Contributor

@kunli2 kunli2 left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! left some comments, otherwise looks good to me.


List<J.ArrayType.AnnotatedDimension> dimensions = new ArrayList<>();
currentNode = node;
while (currentNode instanceof ArrayTypeTree || currentNode instanceof AnnotatedTypeTree) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems the while loop here covers the while loop above (lines 1299-1305), which looks redundant to me, since elemType is the only result we want from the above while loop and it can be fetched from this loop as well.

Copy link
Author

@sfc-gh-dtran sfc-gh-dtran Aug 7, 2023

Choose a reason for hiding this comment

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

Since convert is responsible for scanning the whitespace between tokens using an internal cursor that is incremented left-to-right, it's essential that elemType (which contains the type identifier of the elements in the array type, e.g. the int in int @NotNull [] in line 1307 is converted first since it appears first in the array type.

To collapse this into one while loop, I would have to store the collected annotations (line 1314) and dimensions (line 1319) so that I can call convertAll and sourceBefore after elemType is converted; maybe through something like

dimensions = collectedAnnotations.map(anns -> new AnnotatedDimension(
        convertAll(anns),
        padRight(sourceBefore("["), sourceBefore("]"))
    );

I like that idea since it only needs one traversal of the ArrayTree, but it still involves two loops (or one loop and one map). How does that sound?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, thanks for the explanation, I think both look good to me, seems the 2nd approach (collect everything first) looks better.

dimensions.add(new J.ArrayType.AnnotatedDimension(annotations, dimension));
currentNode = ((ArrayTypeTree) currentNode).getType();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove empty lines

@@ -309,7 +309,7 @@ final class ArrayType implements J, TypeTree, Expression {
TypeTree elementType;

@With
List<JRightPadded<Space>> dimensions;
List<AnnotatedDimension> dimensions;
Copy link
Contributor

Choose a reason for hiding this comment

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

This change is not backwards compatible. While there probably aren't very many callers which need to be updated, there is also the compatibility of the serialization form to consider. While this could be addressed with a dedicated deserializer, we should still compare with alternatives, which are backwards compatible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the inputs here, the backward compatibility of deserialization is also my concern. I'm not sure how to handle this correctly, do you think adding a separate field works? and mark the old field deprecated?

Copy link
Contributor

Choose a reason for hiding this comment

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

I will try to take a look at it in the next few days.

Copy link
Contributor

Choose a reason for hiding this comment

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

This came up again in #3667 ; would it be possible to see this through?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we really need to get this fixed...

@@ -1232,10 +1208,55 @@ public J visitTypeCast(TypeCastTree node, Space fmt) {

@Override
public J visitAnnotatedType(AnnotatedTypeTree node, Space fmt) {
if (node.getUnderlyingType() instanceof ArrayTypeTree) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Update:
I'm looking into issue #3453, and came across this PR.

The AnnotatedTypeTree uses a pos field to retain where the annotation exists on the type.
Annotations that come before the J tree are added to the annotations field in the J.AnnotatedType.

AFAICT, visitAnnotatedArrayType will return a J.ArrayType, which does not have a container for leading annotations. So, this will cause an idempotent print issue for @LeadingAnn Integer @TypeUsePosAnn [] ints

Copy link
Contributor

Choose a reason for hiding this comment

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

We could add new fields if that helps and even delete old ones if we deal with the deserialization compatibility. I would have to take a look again to figure it out, but IIRC we have a nested structure in one place and on the other hand we just have an int reflecting the number of dimensions. To support the annotations we should be using a nested structure.

@traceyyoshima
Copy link
Contributor

Hi @sfc-gh-dtran, we really appreciate your work on this PR and patience with us on addressing the issue.

We've added a new PR that is backwards compatible with older LSTs, updates the GradleParserVisitor, and fixes a handful of bugs that were noticed while implementing the changes. Thank you, again for being a part of OpenRewrite!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request parser-java
Projects
Archived in project
5 participants