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

[core] Tree traversal boundaries #949

Merged
merged 17 commits into from
Mar 29, 2018

Conversation

jsotuyod
Copy link
Member

@jsotuyod jsotuyod commented Mar 3, 2018

@jsotuyod jsotuyod added the in:pmd-internals Affects PMD's internals label Mar 3, 2018
@jsotuyod jsotuyod added this to the 6.2.0 milestone Mar 3, 2018
Copy link
Member

@oowekyala oowekyala left a 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.

@jsotuyod
Copy link
Member Author

jsotuyod commented Mar 4, 2018

@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 findDescendantsOfType, I knew I was missing something. Thanks for spotting that one.

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.

@oowekyala
Copy link
Member

I think we can do this without #905.

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 node.getFirstDescendantOfType(ASTClassOrInterfaceBody.class) if node is in the enclosing class, even though I think it should be considered as behind the boundary -- as is the body of a nested or local class. I'm pretty sure this tricky situation could be resolved by #905, but in the meantime, this could perhaps be worked around by reimplementing traversal methods in AbstractJavaNode to handle anonymous class specially...

This is a correction of a broken behavior.

Yeah ok that makes sense. I should've re-read #904 before posting that comment, sorry ^^

@jsotuyod jsotuyod added the is:WIP For PRs that are not fully ready, or issues that are actively being tackled label Mar 5, 2018
List<ASTClassOrInterfaceDeclaration> packageDecl = cUnit
.findDescendantsOfType(ASTClassOrInterfaceDeclaration.class);
addDeclarations(itemsByLineNumber, packageDecl);
List<ASTClassOrInterfaceDeclaration> classDecl = new ArrayList<>();
Copy link
Member

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

Copy link
Member Author

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

@oowekyala oowekyala changed the title [core] Tree transversal boundaries [core] Tree traversal boundaries Mar 10, 2018
 - This fixes pmd#904
 - `hasDescendantOfType`, `getFirstDescendantOfType` and
`findDescendantsOfType` are now consistent
 - 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
@jsotuyod jsotuyod force-pushed the tree-transversal-boundaries branch from 8675b50 to 240d26e Compare March 12, 2018 04:20
@oowekyala
Copy link
Member

@jsotuyod Let me know when you think this is ready for review

@jsotuyod jsotuyod removed the is:WIP For PRs that are not fully ready, or issues that are actively being tackled label Mar 23, 2018
@jsotuyod
Copy link
Member Author

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.

@adangel adangel modified the milestone: 6.3.0 Mar 26, 2018
@oowekyala oowekyala dismissed their stale review March 26, 2018 16:46

Outdated review

@jsotuyod jsotuyod added this to the 6.3.0 milestone Mar 26, 2018
Copy link
Member

@oowekyala oowekyala left a 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) {
Copy link
Member

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

Copy link
Member Author

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);
Copy link
Member

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

Copy link
Member Author

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")) {
Copy link
Member

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)

Copy link
Member Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in:pmd-internals Affects PMD's internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants