diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index 01a4e86375..4ba4da5471 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -239,9 +239,11 @@ options { PARSER_BEGIN(JavaParser) package net.sourceforge.pmd.lang.java.ast; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import net.sourceforge.pmd.lang.ast.CharStream; +import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.ast.TokenMgrError; public class JavaParser { @@ -484,28 +486,22 @@ public class JavaParser { } } + private void checkForLocalInterfaceOrEnumType() { + if (!isRecordTypeSupported()) { + throwParseException("Local interfaces and enums are only supported with Java 14 Preview and Java 15 Preview"); + } + } + private boolean isRecordTypeSupported() { return (jdkVersion == 14 || jdkVersion == 15) && preview; } - private boolean isRecordDeclarationAhead() { - int amount = 1; - while (amount < 10) { - Token next = getToken(amount); - if (next.kind == IDENTIFIER && "record".equals(next.image)) { - return true; - } - // stop looking ahead at "=" or ";" or "{" - if (next.kind == ASSIGN || next.kind == LBRACE || next.kind == SEMICOLON) { - return false; - } - amount++; - } - return false; + private boolean isSealedClassSupported() { + return jdkVersion == 15 && preview; } private void checkForSealedClassUsage() { - if (jdkVersion != 15 || !preview) { + if (!isSealedClassSupported()) { throwParseException("Sealed Classes are only supported with Java 15 Preview"); } } @@ -573,6 +569,29 @@ public class JavaParser { return false; } + private boolean classModifierLookahead() { + Token next = getToken(1); + return next.kind == AT + || next.kind == PUBLIC + || next.kind == PROTECTED + || next.kind == PRIVATE + || next.kind == ABSTRACT + || next.kind == STATIC + || next.kind == FINAL + || next.kind == STRICTFP + || isSealedClassSupported() && isKeyword("sealed") + || isSealedClassSupported() && isNonSealedModifier(); + } + + private boolean localTypeDeclLookahead() { + Token next = getToken(1); + return next.kind == CLASS + || isRecordTypeSupported() && next.kind == INTERFACE + || isRecordTypeSupported() && next.kind == AT && isToken(2, INTERFACE) + || isRecordTypeSupported() && next.kind == IDENTIFIER && next.image.equals("enum") + || isRecordTypeSupported() && next.kind == IDENTIFIER && next.image.equals("record"); + } + /** * True if we're in a switch block, one precondition for parsing a yield * statement. @@ -1963,42 +1982,63 @@ void Block() : } void BlockStatement(): -{} +{int mods = 0;} { LOOKAHEAD( { isNextTokenAnAssert() } ) AssertStatement() | LOOKAHEAD( { isYieldStart() } ) YieldStatement() -| - LOOKAHEAD(2147483647, ( "final" | Annotation() )* Type() , {isRecordTypeSupported() && !isRecordDeclarationAhead() || !isRecordTypeSupported()}) - LocalVariableDeclaration() ";" -| - LOOKAHEAD(1, {isRecordTypeSupported() && !isRecordDeclarationAhead() || !isRecordTypeSupported()}) - Statement() -| - // we don't need to lookahead further here - // the ambiguity between start of local class and local variable decl - // and start of local record and local variable decl or statement - // is already handled in the lookahead guarding LocalVariableDeclaration - // and Statement above. - LocalClassOrRecordDecl() -} - -void LocalClassOrRecordDecl() #void: -{int mods = 0;} -{ - // this preserves the modifiers of the local class or record. - // it allows for modifiers that are forbidden for local classes and records, - // but anyway we are *not* checking modifiers for incompatibilities - // anywhere else in this grammar (and indeed the production Modifiers - // accepts any modifier explicitly for the purpose of forgiving modifier errors, - // and reporting them later if needed --see its documentation). - - // In particular, it unfortunately allows local class declarations to start - // with a "default" modifier, which introduces an ambiguity with default - // switch labels. This is guarded by a custom lookahead around SwitchLabel +| LOOKAHEAD( "@" | "final" ) + // this eagerly parses all modifiers and annotations. After that, either a local type declaration + // or a local variable declaration follows. + // This allows more modifiers for local variables than actually allowed + // and the annotations for local variables need to be moved in the AST down again. mods=Modifiers() ( - LOOKAHEAD({isKeyword("record")}) RecordDeclaration(mods) - | ClassOrInterfaceDeclaration(mods) + LOOKAHEAD({localTypeDeclLookahead()}) LocalTypeDecl(mods) + | + { + List annotationsAndChildren = new ArrayList(); + while (jjtree.peekNode() instanceof ASTAnnotation) { + annotationsAndChildren.add(jjtree.popNode()); + } + } + LocalVariableDeclaration() + { + ASTLocalVariableDeclaration localVarDecl = (ASTLocalVariableDeclaration) jjtree.peekNode(); + if ((mods & AccessNode.FINAL) == AccessNode.FINAL) { + localVarDecl.setFinal(true); + } + if (!annotationsAndChildren.isEmpty()) { + Collections.reverse(annotationsAndChildren); + for (int i = 0; i < localVarDecl.getNumChildren(); i++) { + annotationsAndChildren.add(localVarDecl.getChild(i)); + } + for (int i = 0; i < annotationsAndChildren.size(); i++) { + Node child = annotationsAndChildren.get(i); + child.jjtSetParent(localVarDecl); + localVarDecl.jjtAddChild(child, i); + } + } + } + ";" + ) +| LOOKAHEAD({classModifierLookahead() || localTypeDeclLookahead()}) + mods=Modifiers() + LocalTypeDecl(mods) +| LOOKAHEAD(Type() ) + LocalVariableDeclaration() ";" +| + Statement() +} + +void LocalTypeDecl(int mods) #void: +{} +{ + ( + LOOKAHEAD() ClassOrInterfaceDeclaration(mods) + | LOOKAHEAD() ClassOrInterfaceDeclaration(mods) { checkForLocalInterfaceOrEnumType(); } + | LOOKAHEAD({isKeyword("record")}) RecordDeclaration(mods) { checkForLocalInterfaceOrEnumType(); } + | LOOKAHEAD({isKeyword("enum")}) EnumDeclaration(mods) { checkForLocalInterfaceOrEnumType(); } + | AnnotationTypeDeclaration(mods) { checkForLocalInterfaceOrEnumType(); } ) } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnnotationTypeDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnnotationTypeDeclaration.java index 0eb92dbf71..75af7118a8 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnnotationTypeDeclaration.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnnotationTypeDeclaration.java @@ -34,6 +34,10 @@ public class ASTAnnotationTypeDeclaration extends AbstractAnyTypeDeclaration { return TypeKind.ANNOTATION; } + @Override + public boolean isLocal() { + return getParent() instanceof ASTBlockStatement; + } @Override public List getDeclarations() { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java index d672a49046..e05c9136d5 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTAnyTypeDeclaration.java @@ -78,6 +78,10 @@ public interface ASTAnyTypeDeclaration extends TypeNode, JavaQualifiableNode, Ac */ boolean isNested(); + /** + * Returns true if this type is declared locally, e.g. in the context of a method block. + */ + boolean isLocal(); /** * The kind of type this node declares. diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceDeclaration.java index db840ddd45..1216b96fca 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceDeclaration.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTClassOrInterfaceDeclaration.java @@ -65,6 +65,7 @@ public class ASTClassOrInterfaceDeclaration extends AbstractAnyTypeDeclaration { * Returns true if the class is declared inside a block other * than the body of another class, or the top level. */ + @Override public boolean isLocal() { if (!isLocalComputed) { Node current = getParent(); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTEnumDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTEnumDeclaration.java index edaa96a0bc..d4fe5cdb33 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTEnumDeclaration.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTEnumDeclaration.java @@ -34,6 +34,10 @@ public class ASTEnumDeclaration extends AbstractAnyTypeDeclaration { return TypeKind.ENUM; } + @Override + public boolean isLocal() { + return getParent() instanceof ASTBlockStatement; + } @Override public List getDeclarations() { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java index 6250e42a59..9549272604 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTRecordDeclaration.java @@ -63,6 +63,7 @@ public final class ASTRecordDeclaration extends AbstractAnyTypeDeclaration { return true; } + @Override public boolean isLocal() { return getParent() instanceof ASTBlockStatement; } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java15PreviewTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java15PreviewTest.java index 985fe08eb4..d6ebe95602 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java15PreviewTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/Java15PreviewTest.java @@ -207,4 +207,25 @@ public class Java15PreviewTest { Assert.assertEquals("TimesExpr", permittedSubclasses.get(2).getImage()); Assert.assertEquals("NegExpr", permittedSubclasses.get(3).getImage()); } + + @Test + public void localInterfaceAndEnums() { + ASTCompilationUnit compilationUnit = java15p.parseResource("LocalInterfacesAndEnums.java"); + List types = compilationUnit.findDescendantsOfType(ASTAnyTypeDeclaration.class); + Assert.assertEquals(5, types.size()); + Assert.assertTrue(types.get(0) instanceof ASTClassOrInterfaceDeclaration); + Assert.assertFalse(types.get(0).isLocal()); + Assert.assertTrue(types.get(1) instanceof ASTClassOrInterfaceDeclaration); + Assert.assertTrue(types.get(2) instanceof ASTClassOrInterfaceDeclaration); + Assert.assertTrue(types.get(3) instanceof ASTEnumDeclaration); + Assert.assertTrue(types.get(4) instanceof ASTAnnotationTypeDeclaration); + for (int i = 1; i < 5; i++) { + Assert.assertTrue(types.get(i).isLocal()); + } + } + + @Test(expected = ParseException.class) + public void localInterfacesAndEnumsBeforeJava15PreviewShouldFail() { + java15.parseResource("LocalInterfacesAndEnums.java"); + } } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalInterfacesAndEnums.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalInterfacesAndEnums.java new file mode 100644 index 0000000000..0897eb27a4 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalInterfacesAndEnums.java @@ -0,0 +1,19 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +public class LocalInterfacesAndEnums { + + { + class MyLocalClass {} + + // static local classes are not allowed (neither Java15 nor Java15 Preview) + //static class MyLocalStaticClass {} + + interface MyLocalInterface {} + + enum MyLocalEnum { A } + + @interface MyLocalAnnotation {} + } +}