-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
[core] Tree traversal boundaries #949
Conversation
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'm not satisfied with the behaviour of that. Now, any call to node.getFirstDescendantOfType will return null if node is a find boundary, eg if it's a body declaration inside an anonymous class, and or a nested ClassOrInterfaceDeclaration. Consequently, nested class declaration traversal logic is inconsistent with outer classes'. For example:
class Outer {
class Nested { // ClassOrInterfaceDeclaration[@FindBoundary=true()]
void method() {}
}
void bar() {}
}
// outer and nested are the ClassOrInterfaceDeclarations
outer.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class) == nested
outer.getFirstDescendantOfType(ASTMethodDeclaration.class) // == void bar()
nested.getFirstDescendantOfType(<anything>) == null
Under those conditions, you're forced to break the boundary with an explicit jjtGetChild call, which is impractical. I think semantically, node.getFirstDescendantOfType
should search the descendants even if node.isFindBoundary()
is true. Otherwise the name doesn't fit the behaviour -- which is to find descendants, regardless of the self node. It should stop recursion on find boundaries though.
Correct me if I'm wrong, but this causes the change, as it currently stands, to be API breaking, even though binary compatible. For example, you had to modify some test cases to make them work. It could break some rules too.
hasDescendantOfType, getFirstDescendantOfType and findDescendantsOfType are now consistent
findDescendantsOfType
still crosses boundaries by default, which is inconsistent with the other methods. There is an "overload" which allows to change the behaviour, but its interface is inconsistent with the others since it works by performing side effects on a list given in parameter instead of returning a result list... This breaks call chains and I still think it should be taken care of
Even if we changed the "true" on the previous link to "false", the behaviour is still incorrect. In that case, on the NestedAnonymousClass type res test case, the following expression returns three results instead of two:
acu.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class)
.findDescendantsOfType(ASTClassOrInterfaceBodyDeclaration.class)
because it crosses the anonymous class' ClassOrInterfaceBody and adds its body declaration. I think the most logical way to treat that is to implement #905 and make that AnonymousClassDeclaration a find boundary. BodyDeclarations shoulnd't be find boundaries. Until #905, I'm not sure how we could make the behaviour consistent though...
...
There are probably other inconsistencies lurking, and I think this PR lacks a test suite to define precisely the expected behaviour. The documentation of the traversal methods could be revised too.
@oowekyala thanks for your comments. You make some very interesting points. As for ignoring self as a find boundary, I had thought about it, but felt it could be a little dangerous... I still have to think it thoroughly to be sure that can't backfire. Non the less, the test would have to be edited, as we were searching for allocations, which are not the boundary, but higher up. As for I think we can do this without #905. I'm still not sure we actually need a specific node type for anonymous classes (even if the problem you report is effectively real), but I admit I haven't been able to properly think it over, so this is mostly my gut speaking. Take it with a grain of salt. I do not think of this as a breaking API change, and for his comment on #904 I don't think @adangel does either. This is a correction of a broken behavior. If people were relying on that broken behavior, sure, things will cease to work for them (and that's why a proper notice in the changelog is in order); but it's still a broken behavior / bug, which should be fixed ASAP. The fact that nothing on our own codebase broke other than a single test is telling on how unlikely this is to happen anyway. I agree non the less, some boundary checking unit tests could aid. I'll try to work them out. |
Maybe by moving the boundary on the AllocationExpression if it's an anonymous class ? The danger here, is that eg the type arguments or constructor arguments would be lost behind the boundary. On the other hand, moving the boundary on the ClassOrInterfaceBody would make that Body be included in searches for
Yeah ok that makes sense. I should've re-read #904 before posting that comment, sorry ^^ |
List<ASTClassOrInterfaceDeclaration> packageDecl = cUnit | ||
.findDescendantsOfType(ASTClassOrInterfaceDeclaration.class); | ||
addDeclarations(itemsByLineNumber, packageDecl); | ||
List<ASTClassOrInterfaceDeclaration> classDecl = new ArrayList<>(); |
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 still think it would be nice to remove that extra statement by adding an overload <T> List<T> findDescendantsOfType(Class<T>, boolean)
. This would be consistent with the signature and usage of all other find*
methods -- which return a list instead of void
. These variables could then be initialized with a single expression
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 has been done on AbstractNode
, but adding it to the interface will have to wait to 7.0.0 (add it to #881 when merging). The same applies to PreserveStackTraceRule
where such a change could not be taken advantage of due to the usage of jjtGetParent
which returns Node
- This fixes pmd#904 - `hasDescendantOfType`, `getFirstDescendantOfType` and `findDescendantsOfType` are now consistent
- Fixes pmd#370
- Also break once we have a result
- A few places actually need to do so, and some other were simply wrong - We can now cross the boundary if searching dowm from it - Anonymous inner classes are still somewhat inconsistent
8675b50
to
240d26e
Compare
@jsotuyod Let me know when you think this is ready for review |
Ok, this should be ready now. The inconsistencies with anonymous inner classes (which predated this PR) remains, but the current grammar doesn't allow a fix. So until we take on #905 on PMD 7.0.0 that will remain as-is. Standing in a find boundary we can now drill down without further overhead, meaning inconsistencies between outer / inner classes are no more. |
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.
LGTM. This is a really nice changeset :) You should just merge master and it would be good to merge
final List<T> list = new ArrayList<>(); | ||
findDescendantsOfType(this, targetType, list, crossBoundaries); | ||
return list; | ||
} | ||
|
||
@Override | ||
public <T> void findDescendantsOfType(Class<T> targetType, List<T> results, boolean crossBoundaries) { |
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 should probably deprecate the public use of that overload in 7.0.0, when the new one is added to the Node interface
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.
good idea, will do before merging
@@ -103,7 +103,7 @@ private String getMethodOrAttributeName(ASTPrimaryExpression node) { | |||
|
|||
|
|||
private boolean isAttributeAccess(ASTPrimaryExpression node) { | |||
return node.findDescendantsOfType(ASTPrimarySuffix.class).isEmpty(); | |||
return !node.hasDescendantOfType(ASTPrimarySuffix.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.
Nice find! We could maybe make a rule for our dogfood ruleset that detects that
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.
Definitely. There is plenty we can do in dogfooding. Avoiding calls to super.visit
on rules using rulechain is at the top my list. We should maybe create an issue to track these...
} | ||
if (result == false) { | ||
final ASTName name = annotation.getFirstDescendantOfType(ASTName.class); | ||
if (name.hasImageEqualTo("VisibleForTesting")) { |
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.
Looks to me like this rule should extend AbstractIgnoredAnnotationRule. Once you merge master you can probably do that (or in another 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.
Definitely another PR, but a great idea non the less. That refactor just keeps paying off
hasDescendantOfType
,getFirstDescendantOfType
andfindDescendantsOfType
are now consistent