-
-
Notifications
You must be signed in to change notification settings - Fork 354
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: consolidate a consistent behavior for CtElement#getParent #3793
fix: consolidate a consistent behavior for CtElement#getParent #3793
Conversation
@dya-tel You're failing some style checks, could you run |
Aaand, now you ran into flaky CI. I'm pretty sure the test failure now is caused by Maven connection pooling issues with Azure (see #3745). You can do an empty commit ( Sorry about that, this has happened often enough now that we probably need to implement a workaround. |
Haha, I don't mind. Seems like it has finally worked. |
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 think this PR looks good overall. I'd definitely prefer standard while loops over do-while loops, as the latter are much less commonly used in Java (and here, the same thing can be achieved with standard while loops without jumping through hoops).
I have one concern, and that's the usability aspect of the getParent()
methods. For the getParent()
(no-argument) method, you are guaranteed that e.getParentInitialized() == true <==> e.getParent() != null
, making the usage very neat. The other overloads have no such guarantee, and you need to double up on checks (both check that the parent is initialized, and that the return value is not null
). There is also a fact that getParent(Class)
and getParent(Filter)
search up the tree, should they throw ParentNotInitialized
if a transitive parent is not initialized? They don't currently, but my first thought when reading the PR was that that was the intended behavior.
I think it makes sense for getParent(Class)
and getParent(Filter)
to return null
instead of throwing an exception. In my mind, they both search through the collection of all parents of the current element and try to find a match, and return null
if no match is found. If the current element has no parent, then that collection is empty (but not non-existent), leading to there being no match, and thus we return null
.
@dya-tel Thoughts? One way or another this PR should be merged (in my opinion), as getParent(Class)
and getParent(Filter)
should at the very least maintain the same behavior. Due to the major revision coming up (see #2869), this is the time to perform a breaking change like this!
Ps. We'll bring in an integrator for the final verdict on this once we've discussed it (I'm not an integrator, was just asked to review this).
CtElement current = this; | ||
do { | ||
current = current.getParent(); | ||
if (parentType.isAssignableFrom(current.getClass())) { | ||
return (P) current; | ||
} | ||
} while (current.isParentInitialized()); |
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.
The same behavior can be achieved with a standard while loop. You can also get rid of the "unchecked" warning by casting with the parent type instead.
CtElement current = this; | |
do { | |
current = current.getParent(); | |
if (parentType.isAssignableFrom(current.getClass())) { | |
return (P) current; | |
} | |
} while (current.isParentInitialized()); | |
CtElement current = getParent(); | |
while (current.isParentInitialized()) { | |
if (parentType.isAssignableFrom(current.getClass())) { | |
return parentType.cast(current); | |
} | |
current = current.getParent(); | |
} |
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.
My suggestion here is incorrect:
The 'do-while' is actually required here. With your 'while' variants if the first parent matches but doesn't have a parent itself, it won't be returned.
do { | ||
current = current.getParent(); | ||
try { | ||
while (current != null && !filter.matches(current)) { | ||
current = (E) current.getParent(); | ||
if (filter.matches((E) current)) { | ||
return (E) current; | ||
} | ||
break; | ||
} catch (ClassCastException e) { | ||
} catch (ClassCastException ignored) { | ||
// expected, some elements are not of type | ||
current = (E) current.getParent(); | ||
} | ||
} | ||
} while (current.isParentInitialized()); |
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.
A standard while loop can do the same thing (couldn't do a suggestion here due to the fragmented diff):
CtElement current = getParent();
while (current.isParentInitialized()) {
try {
if (filter.matches((E) current)) {
return (E) current;
}
} catch (ClassCastException ignored) {
// expected, some elements are not of type
}
current = current.getParent();
};
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.
My suggestion here is incorrect:
The 'do-while' is actually required here. With your 'while' variants if the first parent matches but doesn't have a parent itself, it won't be returned.
if (typeDeclarer.isParentInitialized()) { | ||
typeDeclarer = typeDeclarer.getParent(CtFormalTypeDeclarer.class); | ||
} else { | ||
typeDeclarer = null; |
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.
Why not use the ternary operator here like you've done in the other places?
The 'do-while' is actually required here. With your 'while' variants if the first parent matches but doesn't have a parent itself, it won't be returned. The 'null/exception' decision is a tough one indeed. In the current situation I prefer the 'null' way too, but looking forward the 'exception' way should be better. As to why - well, first of all, because overloads would match the 'main' one's behavior. I also treat the About the transitive parents. The hierarchy must end somewhere, so there's always a transitive parent which has an uninitialized one (in the current meaning). And this will cause an exception if a match is not found. With everything said, I can remake this PR the 'null' way and cleanup the redundant checks for now. And someday when the |
Indeed you are right. Do-while it is.
By the very nature of what the overloads do, this is already not the case. One gets the immediate parent, the other search for ancestors that match a type or lambda. They are fundamentally different things.
Well, that's not really how it works even in this PR. Imagine that I programmatically create a class and add a method. Then the method has the class as a parent, but the class has no parent. If I search for parents with The truly consistent solution would be to throw if no match is found, instead of returning
I'm not convinced of either approach. They both have problems. IMO the best solution would be to rename the overloads to |
@dya-tel Do you agree that these are the options?
I think only 1. and 2. are realistic, but I threw 3. in there because why not. |
Well, in my eyes the overloads do the same thing - search for a parent. The parameter-less one just matches any parent, something along the lines of
Yep, it indeed doesn't work the way I described - it was a "what if" scenario on how it could be made "the good way in my opinion". Let the elements have
It's a pretty bad solution for now from the usability view, but "the only right way" in my "what if scenario".
A good enough solution for now - helps to cleanup Spoon's code and is easy to use in client code. In the "what if scenario" doesn't work at all.
Let's just not. All in all, I'd prefer the second option for now. |
I agree, this is a valid way to view it. Well argued, I now understand your mental model. My mental model is more like
Given your above definition, I also agree that it would be a theoretically pure solution.
That's also probably too breaking, but interesting. The semantics of the exception are unclear, as you've clearly demonstrated.
I agree. It's the pragmatic solution, although I can see now why it's not as intellectually satisfying as your initial approach. |
Well, this was "fun". Here's the null variant. |
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.
Well, this was "fun". Here's the null variant.
I'm sorry if you found it frustrating, but as this is a change to the behavior of publicly accessible functionality, we need to carefully consider the alternatives. Thank you for taking the time to do so.
I think the code now looks excellent, I just feel that it would be prudent to clearly document the return values of the getParent()
overloads. After that, I think it's all good.
@@ -250,13 +250,12 @@ | |||
/** | |||
* Gets the first parent that matches the given type. |
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.
Could you document this with an @return
, in particular that it returns null
if there's no match?
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 is based on the discussion we had previously about the poor documentation of null
returns in Spoon :)
@@ -250,13 +250,12 @@ | |||
/** | |||
* Gets the first parent that matches the given type. | |||
*/ | |||
<P extends CtElement> P getParent(Class<P> parentType) throws ParentNotInitializedException; | |||
<P extends CtElement> P getParent(Class<P> parentType); | |||
|
|||
/** | |||
* Gets the first parent that matches the filter. |
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.
Same as above.
No-no-no, please pay no mind. It's totally fine, I'm just grunting.
Thanks! Though, I didn't do much. |
Co-authored-by: Simon Larsén <[email protected]>
@dya-tel
I think in discussing the different options as carefully as we did, you've made a significant contribution only with that. You've also helped me with practicing code review, which is something I really do need practice with. And the code is good, and the docs much more clear now. So, again, thank you for taking the time :) @monperrus This is ready for your consideration. It's a breaking change. Here's the tl;dr of it all. See the opening post for a description of how the After a lot of discussion, @dya-tel and I came to the conclusion that it is more usable if The breaking behavioral change is therefore that I recommend a merge, it's a good contribution, and we've weighed the pros and cons of the different options carefully. |
We have a major release in front of us, so this is timely. Although we very much value backward-compatibility, the fact that very few tests break is a good indicator that this change is relatively safe. So LGTM, will merge. @dya-tel , thanks a lot for your contribution, @slarse thanks for the shepherding. |
Thanks @dya-tel your contribution is much appreciated! |
The three existing overloads are not consistent in their behavior. There is also an issue with javadoc for
getParent(Filter)
- it states that the method can return the receiving element which would be counter-intuitive if it was actually true (it's not). Some discussion can be seen in #3734.When an element doesn't have a parent:
getParent()
throws ParentNotInitializedExceptiongetParent(Class)
returns nullgetParent(Filter)
throws ParentNotInitializedExceptionWhen there are no matching parents:
getParent()
- not applicable, any parent matchesgetParent(Class)
returns nullgetParent(Filter)
throws ParentNotInitializedException (and, maybe, returns null in some cases)This PR defines the following behavior:
This PR includes a behavior consistency test, matching behavior changes, a javadoc fix and an easier-to-read
getParent(Filter)
implementation.