From 18581b82ae42888acc48e65cc89d9757d8be6c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Fournier?= Date: Tue, 6 Nov 2018 04:22:31 +0100 Subject: [PATCH] Parse ClassOrInterfaceType recursively --- pmd-java/etc/grammar/Java.jjt | 45 +++++++++++++-- .../java/ast/ASTClassOrInterfaceType.java | 36 +++++++++++- .../pmd/lang/java/ast/AbstractJavaNode.java | 56 +++++++++++++++++++ 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index 960c45c9d4..ad8cd1ec9e 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -214,6 +214,8 @@ import java.util.List; import java.util.Map; import net.sourceforge.pmd.lang.ast.CharStream; import net.sourceforge.pmd.lang.ast.TokenMgrError; +import net.sourceforge.pmd.lang.ast.Node; + public class JavaParser { private int jdkVersion = 0; @@ -1987,18 +1989,51 @@ void ReferenceType() #void: | ( ClassOrInterfaceType() [ LOOKAHEAD(2) Dims() ] ) #ArrayType(>1) } -void ClassOrInterfaceType(): +void ClassOrInterfaceType() #void: { StringBuilder s = new StringBuilder(); Token t; } { - t= {s.append(t.image);} - [ LOOKAHEAD(2) TypeArguments() ] - ( LOOKAHEAD(2) "." t= {s.append('.').append(t.image);} [ LOOKAHEAD(2) TypeArguments() ] )* - {jjtThis.setImage(s.toString());} + ( + ( + t= { s.append(t.getImage()); } + // We gobble up all identifiers until we find + // either type arguments or annotations, because + // it may be a FQCN + ( LOOKAHEAD("." ) + "." t= { s.append('.').append(t.getImage()); } + )* + ) + [ LOOKAHEAD( "<" ) TypeArguments() ] + ) #ClassOrInterfaceType + { + // At this point the first ClassOrInterfaceType is on top of the stack + jjtree.peekNode().setImage(s.toString()); + } + + + ( LOOKAHEAD(2) + ( + "." + (TypeAnnotation())* + t= { jjtThis.setImage(t.getImage()); } + [ LOOKAHEAD( "<" ) TypeArguments() ] + ) #ClassOrInterfaceType { + // At this point the newer ClassOrInterfaceType has been pushed on the stack + // We retrieve it + AbstractJavaNode lastSegment = (AbstractJavaNode) jjtree.popNode(); + // All its children (annotations and type arguments) have already been popped, + // so the node remaining on top of the stack is the previous segment + Node previousSegment = jjtree.popNode(); + + lastSegment.insertChild((JavaNode) previousSegment, 0, true); + jjtree.pushNode(lastSegment); + } + )* } + void TypeArguments(): {} { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceType.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceType.java index d57a949cde..edc4df5def 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceType.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceType.java @@ -5,15 +5,34 @@ package net.sourceforge.pmd.lang.java.ast; +import java.util.Optional; + +// @formatter:off /** * Represents a class or interface type, possibly parameterised with type arguments. + * This node comes in two productions (see below): + * + *

The first is a left-recursive variant, allowing to parse references to type members + * unambiguously. The resulting node's {@linkplain #getLeftHandSide() left-hand-side type} + * addresses the type parent of the type. The position of type arguments and annotations are + * preserved. + * + *

Parsing types left-recursively has a caveat though: fully-qualified type names. The + * parser can't disambiguate between a reference to a type member (e.g. {@code Map.Entry}, where + * both segments refer to a type, and which would ideally be parsed left-recursively), and a + * qualified type name (e.g. {@code java.util.List}, where the full sequence refers to a unique + * type, but individual segments don't). + * + *

We could remove that with a later AST visit, like type resolution though! * *

  *
- * ClassOrInterfaceType ::= <IDENTIFIER> {@linkplain ASTTypeArguments TypeArguments}? ( "." <IDENTIFIER>  {@linkplain ASTTypeArguments TypeArguments}? )*
+ * ClassOrInterfaceType ::= ClassOrInterfaceType ( "." {@linkplain ASTAnnotation Annotation}* <IDENTIFIER> {@linkplain ASTTypeArguments TypeArguments}? )+
+ *                        | <IDENTIFIER> ( "." <IDENTIFIER> ) *  {@linkplain ASTTypeArguments TypeArguments}?
  *
  * 
*/ +// @formatter:on public class ASTClassOrInterfaceType extends AbstractJavaTypeNode implements ASTReferenceType { public ASTClassOrInterfaceType(String identifier) { super(JavaParserTreeConstants.JJTCLASSORINTERFACETYPE); @@ -31,6 +50,17 @@ public class ASTClassOrInterfaceType extends AbstractJavaTypeNode implements AST } + /** + * Gets the left-hand side type of this type. This is a type we know for sure + * that this type is a member of. + * + * @return A type, or null if this is a base type + */ + public Optional getLeftHandSide() { + return Optional.ofNullable(getFirstChildOfType(ASTClassOrInterfaceType.class)); + } + + @Override public Object jjtAccept(JavaParserVisitor visitor, Object data) { return visitor.visit(this, data); @@ -48,8 +78,8 @@ public class ASTClassOrInterfaceType extends AbstractJavaTypeNode implements AST * same compilation unit - either a class/interface or a enum type. You want * to check this, if {@link #getType()} is null. * - * @return true if this node referencing a type in the same - * compilation unit, false otherwise. + * @return {@code true} if this node referencing a type in the same + * compilation unit, {@code false} otherwise. */ public boolean isReferenceToClassSameCompilationUnit() { ASTCompilationUnit root = getFirstParentOfType(ASTCompilationUnit.class); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AbstractJavaNode.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AbstractJavaNode.java index f3941fb6fc..81783e488a 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AbstractJavaNode.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AbstractJavaNode.java @@ -8,21 +8,25 @@ import net.sourceforge.pmd.lang.ast.AbstractNode; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.symboltable.Scope; + public abstract class AbstractJavaNode extends AbstractNode implements JavaNode { protected JavaParser parser; private Scope scope; private Comment comment; + public AbstractJavaNode(int id) { super(id); } + public AbstractJavaNode(JavaParser parser, int id) { super(id); this.parser = parser; } + @Override public void jjtOpen() { if (beginLine == -1 && parser.token.next != null) { @@ -31,6 +35,7 @@ public abstract class AbstractJavaNode extends AbstractNode implements JavaNode } } + @Override public void jjtClose() { if (beginLine == -1 && children.length == 0) { @@ -43,6 +48,7 @@ public abstract class AbstractJavaNode extends AbstractNode implements JavaNode endColumn = parser.token.endColumn; } + /** * Accept the visitor. * */ @@ -88,20 +94,70 @@ public abstract class AbstractJavaNode extends AbstractNode implements JavaNode return scope; } + @Override public void setScope(Scope scope) { this.scope = scope; } + public void comment(Comment theComment) { comment = theComment; } + public Comment comment() { return comment; } + // insert a child at a given index, shifting other children if need be + // The implementation of jjtAddChild in AbstractNode overwrites nodes + // -> probably unexpected and to be changed + // parser only + void insertChild(JavaNode child, int index, boolean expandTextSpan) { + // Allow to insert a child at random index without overwriting + // If the child is null, it is replaced. If it is not null, children are shifted + if (children != null && index < children.length && children[index] != null) { + Node[] newChildren = new Node[children.length + 1]; + + // toShift nodes are to the right of the insertion index + int toShift = children.length - index; + + // copy the nodes before + System.arraycopy(children, 0, newChildren, 0, index); + + // copy the nodes after + System.arraycopy(children, index, newChildren, index + 1, toShift); + children = newChildren; + } + super.jjtAddChild(child, index); + child.jjtSetParent(this); + + // The text coordinates of this node will be enlarged with those of the child + if (expandTextSpan) { + AbstractJavaNode childImpl = (AbstractJavaNode) child; + + if (this.beginLine > childImpl.beginLine) { + this.beginLine = childImpl.beginLine; + this.beginColumn = childImpl.beginColumn; + } else if (this.beginLine == childImpl.beginLine + && this.beginColumn > childImpl.beginColumn) { + this.beginColumn = childImpl.beginColumn; + } + + if (this.endLine < childImpl.endLine) { + this.endLine = childImpl.endLine; + this.endColumn = childImpl.endColumn; + } else if (this.endLine == childImpl.endLine + && this.endColumn < childImpl.endColumn) { + this.endColumn = childImpl.endColumn; + } + + // TODO tokens + } + } + @Override public final String getXPathNodeName() {