diff --git a/docs/pages/7_0_0_release_notes.md b/docs/pages/7_0_0_release_notes.md index de38d0b896..7893a725fa 100644 --- a/docs/pages/7_0_0_release_notes.md +++ b/docs/pages/7_0_0_release_notes.md @@ -46,6 +46,25 @@ Support for XPath versions 1.0, 1.0-compatibility was removed, support for XPath * The deprecated support for sequence-valued attributes is removed. Sequence-valued properties are still supported. * Refer to [the Saxonica documentation](https://www.saxonica.com/html/documentation/expressions/xpath31new.html) for an introduction to new features in XPath 3.1. + +#### Node stream API + +This version includes a powerful API to navigate trees, similar in usage to the Java 8 Stream API: +```java +node.descendants(ASTMethodCall.class) + .filter(m -> "toString".equals(m.getMethodName())) + .map(m -> m.getQualifier()) + .filter(q -> TypeTestUtil.isA(String.class, q)) + .foreach(System.out::println); +``` + +A pipeline like shown here traverses the tree lazily, which is more efficient than traversing eagerly to put all descendants in a list. It is also much easier to change than the old imperative way. + +To make this API as accessible as possible, the {% jdoc core::lang.ast.Node %} interface has been fitted with new methods producing node streams. Those methods replace previous tree traversal methods like `Node#findDescendantsOfType`. In all cases, they should be more efficient and more convenient. + +See {% jdoc core::lang.ast.NodeStream %} for more details. + + #### JavaScript support The JS specific parser options have been removed. The parser now always retains comments and uses version ES6. diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Node.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Node.java index d3ab5b8066..d5e6ed3be6 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Node.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/Node.java @@ -13,10 +13,11 @@ import java.util.Objects; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import net.sourceforge.pmd.annotation.DeprecatedUntil700; import net.sourceforge.pmd.lang.ast.NodeStream.DescendantNodeStream; import net.sourceforge.pmd.lang.ast.internal.StreamImpl; import net.sourceforge.pmd.lang.rule.xpath.Attribute; -import net.sourceforge.pmd.lang.rule.xpath.DeprecatedAttribute; +import net.sourceforge.pmd.lang.rule.xpath.NoAttribute; import net.sourceforge.pmd.lang.rule.xpath.XPathVersion; import net.sourceforge.pmd.lang.rule.xpath.impl.AttributeAxisIterator; import net.sourceforge.pmd.lang.rule.xpath.impl.XPathHandler; @@ -30,10 +31,14 @@ import net.sourceforge.pmd.util.DataMap.DataKey; * Root interface for all AST nodes. This interface provides only the API * shared by all AST implementations in PMD language modules. This includes for now: * * *

Every language implementation must publish a sub-interface of Node @@ -59,7 +64,12 @@ public interface Node { * Returns a string token, usually filled-in by the parser, which describes some textual characteristic of this * node. This is usually an identifier, but you should check that using the Designer. On most nodes though, this * method returns {@code null}. + * + * @deprecated Should be replaced with methods that have more specific + * names in node classes. */ + @Deprecated + @DeprecatedUntil700 default String getImage() { return null; } @@ -69,7 +79,11 @@ public interface Node { * Returns true if this node's image is equal to the given string. * * @param image The image to check + * + * @deprecated See {@link #getImage()} */ + @Deprecated + @DeprecatedUntil700 default boolean hasImageEqualTo(String image) { return Objects.equals(getImage(), image); } @@ -116,7 +130,7 @@ public interface Node { * * @see DescendantNodeStream#crossFindBoundaries(boolean) */ - @DeprecatedAttribute + @NoAttribute default boolean isFindBoundary() { return false; } @@ -131,7 +145,11 @@ public interface Node { * @param n how many ancestors to iterate over. * @return the n-th parent or null. * @throws IllegalArgumentException if {@code n} is negative or zero. + * + * @deprecated Use node stream methods: {@code node.ancestors().get(n-1)} */ + @Deprecated + @DeprecatedUntil700 default Node getNthParent(int n) { return ancestors().get(n - 1); } @@ -142,7 +160,11 @@ public interface Node { * @param parentType Class literal of the type you want to find * @param The type you want to find * @return Node of type parentType. Returns null if none found. + * + * @deprecated Use node stream methods: {@code node.ancestors(parentType).first()} */ + @Deprecated + @DeprecatedUntil700 default T getFirstParentOfType(Class parentType) { return this.ancestors(parentType).first(); } @@ -154,7 +176,12 @@ public interface Node { * @param parentType Class literal of the type you want to find * @param The type you want to find * @return List of parentType instances found. + * + * @deprecated Use node stream methods: {@code node.ancestors(parentType).toList()}. + * Most usages don't really need a list though, eg you can iterate the node stream instead */ + @Deprecated + @DeprecatedUntil700 default List getParentsOfType(Class parentType) { return this.ancestors(parentType).toList(); } @@ -166,7 +193,12 @@ public interface Node { * @param childType class which you want to find. * @return List of all children of type childType. Returns an empty list if none found. * @see #findDescendantsOfType(Class) if traversal of the entire tree is needed. + * + * @deprecated Use node stream methods: {@code node.children(childType).toList()}. + * Most usages don't really need a list though, eg you can iterate the node stream instead */ + @Deprecated + @DeprecatedUntil700 default List findChildrenOfType(Class childType) { return this.children(childType).toList(); } @@ -178,7 +210,12 @@ public interface Node { * * @param targetType class which you want to find. * @return List of all children of type targetType. Returns an empty list if none found. + * + * @deprecated Use node stream methods: {@code node.descendants(targetType).toList()}. + * Most usages don't really need a list though, eg you can iterate the node stream instead */ + @Deprecated + @DeprecatedUntil700 default List findDescendantsOfType(Class targetType) { return this.descendants(targetType).toList(); } @@ -194,7 +231,12 @@ public interface Node { * if false, recursion stops for nodes for which * {@link #isFindBoundary()} is true * @return List of all matching descendants + * + * @deprecated Use node stream methods: {@code node.descendants(targetType).crossFindBoundaries(b).toList()}. + * Most usages don't really need a list though, eg you can iterate the node stream instead */ + @Deprecated + @DeprecatedUntil700 default List findDescendantsOfType(Class targetType, boolean crossFindBoundaries) { return this.descendants(targetType).crossFindBoundaries(crossFindBoundaries).toList(); } @@ -205,9 +247,13 @@ public interface Node { * @param childType class which you want to find. * @return Node of type childType. Returns null if none found. * @see #getFirstDescendantOfType(Class) if traversal of the entire tree is needed. + * + * @deprecated Use {@link #firstChild(Class)} */ + @Deprecated + @DeprecatedUntil700 default T getFirstChildOfType(Class childType) { - return children(childType).first(); + return firstChild(childType); } @@ -217,7 +263,11 @@ public interface Node { * * @param descendantType class which you want to find. * @return Node of type descendantType. Returns null if none found. + * + * @deprecated Use node stream methods: {@code node.descendants(targetType).first()}. */ + @Deprecated + @DeprecatedUntil700 default T getFirstDescendantOfType(Class descendantType) { return descendants(descendantType).first(); } @@ -227,7 +277,11 @@ public interface Node { * * @param type the node type to search * @return true if there is at least one descendant of the given type + * + * @deprecated Use node stream methods: {@code node.descendants(targetType).nonEmpty()}. */ + @Deprecated + @DeprecatedUntil700 default boolean hasDescendantOfType(Class type) { return descendants(type).nonEmpty(); } @@ -258,6 +312,8 @@ public interface Node { * Returns a data map used to store additional information on this node. * * @return The user data map of this node + * + * @since 6.22.0 */ DataMap> getUserMap(); @@ -267,6 +323,8 @@ public interface Node { * of the tree. * * @return The parent of this node + * + * @since 6.21.0 */ Node getParent(); @@ -275,12 +333,15 @@ public interface Node { * Returns the child of this node at the given index. * * @throws IndexOutOfBoundsException if the index is negative or greater than {@link #getNumChildren()}. + * @since 6.21.0 */ Node getChild(int index); /** * Returns the number of children of this node. + * + * @since 6.21.0 */ int getNumChildren(); @@ -289,6 +350,8 @@ public interface Node { * node is a {@linkplain RootNode root node}, returns -1. * * @return The index of this node in its parent's children + * + * @since 6.21.0 */ int getIndexInParent(); @@ -313,6 +376,8 @@ public interface Node { * a different override per concrete node class (no shortcuts). * * The default implementation calls back {@link AstVisitor#cannotVisit(Node, Object)}. + * + * @since 7.0.0 */ default R acceptVisitor(AstVisitor visitor, P data) { return visitor.cannotVisit(this, data); @@ -349,6 +414,8 @@ public interface Node { /** * Returns the first child of this node, or null if it doesn't exist. + * + * @since 7.0.0 */ default @Nullable Node getFirstChild() { return getNumChildren() > 0 ? getChild(0) : null; @@ -357,12 +424,42 @@ public interface Node { /** * Returns the first last of this node, or null if it doesn't exist. + * + * @since 7.0.0 */ default @Nullable Node getLastChild() { return getNumChildren() > 0 ? getChild(getNumChildren() - 1) : null; } + /** + * Returns the previous sibling of this node, or null if it does not exist. + * + * @since 7.0.0 + */ + default @Nullable Node getPreviousSibling() { + Node parent = getParent(); + int idx = getIndexInParent(); + if (parent != null && idx > 0) { + return parent.getChild(idx - 1); + } + return null; + } + + /** + * Returns the next sibling of this node, or null if it does not exist. + * + * @since 7.0.0 + */ + default @Nullable Node getNextSibling() { + Node parent = getParent(); + int idx = getIndexInParent(); + if (parent != null && idx < parent.getNumChildren()) { + return parent.getChild(idx + 1); + } + return null; + } + /** * Returns a node stream containing only this node. * {@link NodeStream#of(Node)} is a null-safe version @@ -371,6 +468,7 @@ public interface Node { * @return A node stream containing only this node * * @see NodeStream#of(Node) + * @since 7.0.0 */ default NodeStream asStream() { return StreamImpl.singleton(this); @@ -383,6 +481,7 @@ public interface Node { * you'll probably want to use {@link #children(Class)}. * * @see NodeStream#children(Class) + * @since 7.0.0 */ default NodeStream children() { return StreamImpl.children(this); @@ -396,6 +495,7 @@ public interface Node { * @return A node stream of the descendants of this node * * @see NodeStream#descendants() + * @since 7.0.0 */ default DescendantNodeStream descendants() { return StreamImpl.descendants(this); @@ -409,6 +509,7 @@ public interface Node { * @return A node stream of the whole subtree topped by this node * * @see NodeStream#descendantsOrSelf() + * @since 7.0.0 */ default DescendantNodeStream descendantsOrSelf() { return StreamImpl.descendantsOrSelf(this); @@ -423,6 +524,7 @@ public interface Node { * @return A node stream of the ancestors of this node * * @see NodeStream#ancestors() + * @since 7.0.0 */ default NodeStream ancestors() { return StreamImpl.ancestors(this); @@ -437,6 +539,7 @@ public interface Node { * @return A stream of ancestors * * @see NodeStream#ancestorsOrSelf() + * @since 7.0.0 */ default NodeStream ancestorsOrSelf() { return StreamImpl.ancestorsOrSelf(this); @@ -453,11 +556,31 @@ public interface Node { * @return A new node stream * * @see NodeStream#children(Class) + * @since 7.0.0 */ default NodeStream children(Class rClass) { return StreamImpl.children(this, rClass); } + /** + * Returns the first child of this node that has the given type. + * Returns null if no such child exists. + * + *

If you want to process this element as a node stream, use + * {@code asStream().firstChild(rClass)} instead, which returns + * a node stream. + * + * @param rClass Type of the child to find + * @param Type of the child to find + * + * @return A child, or null + * + * @since 7.0.0 + */ + default @Nullable R firstChild(Class rClass) { + return children(rClass).first(); + } + /** * Returns a {@linkplain NodeStream node stream} of the {@linkplain #descendants() descendants} @@ -470,6 +593,7 @@ public interface Node { * @return A new node stream * * @see NodeStream#descendants(Class) + * @since 7.0.0 */ default DescendantNodeStream descendants(Class rClass) { return StreamImpl.descendants(this, rClass); @@ -477,8 +601,8 @@ public interface Node { /** - * Returns the {@linkplain #ancestors() ancestor stream} of each node - * in this stream, filtered by the given node type. + * Returns the {@linkplain #ancestors() ancestor stream} of this node + * filtered by the given node type. * * @param rClass Type of node the returned stream should contain * @param Type of node the returned stream should contain @@ -486,6 +610,7 @@ public interface Node { * @return A new node stream * * @see NodeStream#ancestors(Class) + * @since 7.0.0 */ default NodeStream ancestors(Class rClass) { return StreamImpl.ancestors(this, rClass); @@ -493,9 +618,10 @@ public interface Node { /** * Returns the root of the tree this node is declared in. + * + * @since 7.0.0 */ - @NonNull - default RootNode getRoot() { + default @NonNull RootNode getRoot() { Node r = this; while (r.getParent() != null) { r = r.getParent(); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/AbstractNode.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/AbstractNode.java index d65d4dd231..9d3839d411 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/AbstractNode.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/AbstractNode.java @@ -183,4 +183,20 @@ public abstract class AbstractNode, N extends Gener return getXPathNodeName(); } + @Override + @SuppressWarnings("unchecked") + public final @Nullable R firstChild(Class rClass) { + // This operation is extremely common so we give it an optimal + // implementation, based directly on the array. This will never + // create a node stream object, and array bounds are not checked. + // It's final so it can be inlined. + for (Node child : children) { + if (rClass.isInstance(child)) { + // rClass.cast(child) is more expensive than this + // unchecked cast, which we know is safe. + return (R) child; + } + } + return null; + } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/GenericNode.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/GenericNode.java index 9e277b59b0..347d835f5a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/GenericNode.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/impl/GenericNode.java @@ -83,4 +83,14 @@ public interface GenericNode> extends Node { default NodeStream ancestors() { return (NodeStream) Node.super.ancestors(); } + + @Override + default @Nullable N getPreviousSibling() { + return (N) Node.super.getPreviousSibling(); + } + + @Override + default @Nullable N getNextSibling() { + return (N) Node.super.getNextSibling(); + } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/internal/SingletonNodeStream.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/internal/SingletonNodeStream.java index 038a44e6b0..269574254e 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/internal/SingletonNodeStream.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ast/internal/SingletonNodeStream.java @@ -162,7 +162,7 @@ final class SingletonNodeStream extends IteratorBasedNStream @Override public NodeStream firstChild(Class rClass) { - return NodeStream.of(TraversalUtils.getFirstChildMatching(node, Filtermap.isInstance(rClass), 0, node.getNumChildren())); + return NodeStream.of(node.firstChild(rClass)); } @Override