diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index 93428c162a..1ba6dd9fd3 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -4,6 +4,7 @@ * Support Pattern Matching for instanceof with Java 15 Preview. * Support Records with Java 15 Preview. * Support Local Records with Java 15 Preview. + * Support Sealed Classes with Java 15 Preview. * Andreas Dangel 08/2020 *==================================================================== * Add support for record types introduced as a preview language @@ -420,9 +421,18 @@ public class JavaParser { if (jdkVersion >= 10 && "var".equals(image)) { throwParseException("With JDK 10, 'var' is a restricted local variable type and cannot be used for type declarations!"); } + if (jdkVersion >= 14 && "yield".equals(image)) { + throwParseException("With JDK 14, 'yield' is a restricted local variable type and cannot be used for type declarations!"); + } if (jdkVersion >= 14 && preview && "record".equals(image)) { throwParseException("With JDK 14 Preview and JDK 15 Preview, 'record' is a restricted identifier and cannot be used for type declarations!"); } + if (jdkVersion >= 15 && preview && "sealed".equals(image)) { + throwParseException("With JDK 15 Preview, 'sealed' is a restricted identifier and cannot be used for type declarations!"); + } + if (jdkVersion >= 15 && preview && "permits".equals(image)) { + throwParseException("With JDK 15 Preview, 'permits' is a restricted identifier and cannot be used for type declarations!"); + } } private void checkForMultipleCaseLabels() { if (jdkVersion < 14) { @@ -494,6 +504,12 @@ public class JavaParser { return false; } + private void checkForSealedClassUsage() { + if (jdkVersion != 15 || !preview) { + throwParseException("Sealed Classes are only supported with Java 15 Preview"); + } + } + // This is a semantic LOOKAHEAD to determine if we're dealing with an assert // Note that this can't be replaced with a syntactic lookahead // since "assert" isn't a string literal token @@ -519,6 +535,12 @@ public class JavaParser { /** * Semantic lookahead to check if the next identifier is a * specific restricted keyword. + * + *

Restricted keywords are: + * var, yield, record, sealed, permits + * + *

enum and assert is used like restricted keywords, as they were not keywords + * in the early java versions. */ private boolean isKeyword(String keyword) { return getToken(1).kind == IDENTIFIER && getToken(1).image.equals(keyword); @@ -1002,6 +1024,14 @@ TOKEN : | < GT: ">" > } +// Note: New token need to be added at the very end in order to +// keep (binary) compatibility with the generated token ids +// see JavaParserConstants +TOKEN : +{ + < NON_SEALED: "non-sealed" > +} + /***************************************** * THE JAVA LANGUAGE GRAMMAR STARTS HERE * *****************************************/ @@ -1073,6 +1103,8 @@ int Modifiers() #void: | "volatile" { modifiers |= AccessNode.VOLATILE; } | "strictfp" { modifiers |= AccessNode.STRICTFP; } | "default" { modifiers |= AccessNode.DEFAULT; checkForBadDefaultImplementationUsage(); } + | LOOKAHEAD({isKeyword("sealed")}) { modifiers |= AccessNode.SEALED; checkForSealedClassUsage(); } + | "non-sealed" { modifiers |= AccessNode.NON_SEALED; checkForSealedClassUsage(); } | Annotation() ) )* @@ -1114,6 +1146,7 @@ void ClassOrInterfaceDeclaration(int modifiers): [ TypeParameters() ] [ ExtendsList() ] [ ImplementsList() ] + [ LOOKAHEAD({isKeyword("permits")}) PermittedSubclasses() ] ClassOrInterfaceBody() { inInterface = inInterfaceOld; } // always restore the flag after leaving the node } @@ -1134,6 +1167,21 @@ void ImplementsList(): ( "," (TypeAnnotation())* ClassOrInterfaceType() )* } +void PermittedSubclasses(): +{ + Token t; + checkForSealedClassUsage(); +} +{ + t = { + if (!"permits".equals(t.image)) { + throw new ParseException("ERROR: expecting permits"); + } + } + (TypeAnnotation())* ClassOrInterfaceType() + ( "," (TypeAnnotation())* ClassOrInterfaceType() )* +} + void EnumDeclaration(int modifiers): { 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 936083b83b..db840ddd45 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 @@ -7,6 +7,7 @@ package net.sourceforge.pmd.lang.java.ast; import java.util.Collections; import java.util.List; +import net.sourceforge.pmd.annotation.Experimental; import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.util.CollectionUtil; @@ -140,4 +141,23 @@ public class ASTClassOrInterfaceDeclaration extends AbstractAnyTypeDeclaration { return it == null ? Collections.emptyList() : CollectionUtil.toList(it.iterator()); } + @Experimental + public List getPermittedSubclasses() { + ASTPermittedSubclasses permitted = getFirstChildOfType(ASTPermittedSubclasses.class); + return permitted == null + ? Collections.emptyList() + : CollectionUtil.toList(permitted.iterator()); + } + + @Experimental + public boolean isSealed() { + int modifiers = getModifiers(); + return (modifiers & AccessNode.SEALED) == AccessNode.SEALED; + } + + @Experimental + public boolean isNonSealed() { + int modifiers = getModifiers(); + return (modifiers & AccessNode.NON_SEALED) == AccessNode.NON_SEALED; + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPermittedSubclasses.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPermittedSubclasses.java new file mode 100644 index 0000000000..afdb68b442 --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/ASTPermittedSubclasses.java @@ -0,0 +1,45 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.ast; + +import java.util.Iterator; + +import net.sourceforge.pmd.annotation.Experimental; + + +/** + * Represents the {@code permits} clause of a (sealed) class declaration. + * + *

This is a Java 15 Preview feature. + * + *

See https://openjdk.java.net/jeps/360 + * + *

+ *  PermittedSubclasses ::= "permits" (TypeAnnotation)* ClassOrInterfaceType
+ *                ( "," (TypeAnnotation)* ClassOrInterfaceType )*
+ * 
+ */ +@Experimental +public final class ASTPermittedSubclasses extends AbstractJavaNode implements Iterable { + + ASTPermittedSubclasses(int id) { + super(id); + } + + ASTPermittedSubclasses(JavaParser p, int id) { + super(p, id); + } + + @Override + public Object jjtAccept(JavaParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + + @Override + public Iterator iterator() { + return new NodeChildrenIterator<>(this, ASTClassOrInterfaceType.class); + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AccessNode.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AccessNode.java index 803d1f6f73..2685fb0ac2 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AccessNode.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AccessNode.java @@ -24,6 +24,8 @@ public interface AccessNode extends Node { int VOLATILE = 0x0200; int STRICTFP = 0x1000; int DEFAULT = 0x2000; + int SEALED = 0x4000; + int NON_SEALED = 0x8000; int getModifiers(); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserDecoratedVisitor.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserDecoratedVisitor.java index ab614443bc..59c4ac6071 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserDecoratedVisitor.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserDecoratedVisitor.java @@ -942,4 +942,11 @@ public class JavaParserDecoratedVisitor implements JavaParserVisitor { visitor.visit(node, data); return visit((JavaNode) node, data); } + + @Override + @Experimental + public Object visit(ASTPermittedSubclasses node, Object data) { + visitor.visit(node, data); + return visit((JavaNode) node, data); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java index fc0cc8e59f..a751ca07af 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorAdapter.java @@ -663,4 +663,10 @@ public class JavaParserVisitorAdapter implements JavaParserVisitor { public Object visit(ASTRecordConstructorDeclaration node, Object data) { return visit((JavaNode) node, data); } + + @Override + @Experimental + public Object visit(ASTPermittedSubclasses node, Object data) { + return visit((JavaNode) node, data); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorDecorator.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorDecorator.java index e56a5b0925..e25d835e14 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorDecorator.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/JavaParserVisitorDecorator.java @@ -795,4 +795,10 @@ public class JavaParserVisitorDecorator implements JavaParserControllessVisitor public Object visit(ASTRecordConstructorDeclaration node, Object data) { return visitor.visit(node, data); } + + @Override + @Experimental + public Object visit(ASTPermittedSubclasses node, Object data) { + return visitor.visit(node, data); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/AbstractJavaRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/AbstractJavaRule.java index 867192a5cf..722ae15fda 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/AbstractJavaRule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/AbstractJavaRule.java @@ -87,6 +87,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTNameList; import net.sourceforge.pmd.lang.java.ast.ASTNormalAnnotation; import net.sourceforge.pmd.lang.java.ast.ASTNullLiteral; import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTPermittedSubclasses; import net.sourceforge.pmd.lang.java.ast.ASTPostfixExpression; import net.sourceforge.pmd.lang.java.ast.ASTPreDecrementExpression; import net.sourceforge.pmd.lang.java.ast.ASTPreIncrementExpression; @@ -871,4 +872,10 @@ public abstract class AbstractJavaRule extends AbstractRule implements JavaParse public Object visit(ASTRecordConstructorDeclaration node, Object data) { return visit((JavaNode) node, data); } + + @Override + @Experimental + public Object visit(ASTPermittedSubclasses node, Object data) { + return visit((JavaNode) node, data); + } } 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 ac324726f2..985fe08eb4 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 @@ -164,4 +164,47 @@ public class Java15PreviewTest { Assert.assertEquals("MerchantSales", records.get(0).getSimpleName()); Assert.assertTrue(records.get(0).isLocal()); } + + @Test(expected = ParseException.class) + public void sealedClassBeforeJava15Preview() { + java15.parseResource("geometry/Shape.java"); + } + + @Test + public void sealedClass() { + ASTCompilationUnit compilationUnit = java15p.parseResource("geometry/Shape.java"); + ASTClassOrInterfaceDeclaration classDecl = compilationUnit.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class); + Assert.assertTrue(classDecl.isSealed()); + List permittedSubclasses = classDecl.getPermittedSubclasses(); + Assert.assertEquals(3, permittedSubclasses.size()); + Assert.assertEquals("Circle", permittedSubclasses.get(0).getImage()); + Assert.assertEquals("Rectangle", permittedSubclasses.get(1).getImage()); + Assert.assertEquals("Square", permittedSubclasses.get(2).getImage()); + } + + @Test + public void nonSealedClass() { + ASTCompilationUnit compilationUnit = java15p.parseResource("geometry/Square.java"); + ASTClassOrInterfaceDeclaration classDecl = compilationUnit.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class); + Assert.assertTrue(classDecl.isNonSealed()); + Assert.assertEquals(0, classDecl.getPermittedSubclasses().size()); + } + + @Test(expected = ParseException.class) + public void sealedInterfaceBeforeJava15Preview() { + java15.parseResource("expression/Expr.java"); + } + + @Test + public void sealedInterface() { + ASTCompilationUnit compilationUnit = java15p.parseResource("expression/Expr.java"); + ASTClassOrInterfaceDeclaration interfaceDecl = compilationUnit.getFirstDescendantOfType(ASTClassOrInterfaceDeclaration.class); + Assert.assertTrue(interfaceDecl.isSealed()); + List permittedSubclasses = interfaceDecl.getPermittedSubclasses(); + Assert.assertEquals(4, permittedSubclasses.size()); + Assert.assertEquals("ConstantExpr", permittedSubclasses.get(0).getImage()); + Assert.assertEquals("PlusExpr", permittedSubclasses.get(1).getImage()); + Assert.assertEquals("TimesExpr", permittedSubclasses.get(2).getImage()); + Assert.assertEquals("NegExpr", permittedSubclasses.get(3).getImage()); + } } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/ConstantExpr.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/ConstantExpr.java new file mode 100644 index 0000000000..800fc3d2d1 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/ConstantExpr.java @@ -0,0 +1,9 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.expression; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class ConstantExpr implements Expr { } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/Expr.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/Expr.java new file mode 100644 index 0000000000..7218bd477f --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/Expr.java @@ -0,0 +1,10 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.expression; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public sealed interface Expr + permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/NegExpr.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/NegExpr.java new file mode 100644 index 0000000000..35194d1cb9 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/NegExpr.java @@ -0,0 +1,9 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.expression; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class NegExpr implements Expr { } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/PlusExpr.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/PlusExpr.java new file mode 100644 index 0000000000..ac652798e2 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/PlusExpr.java @@ -0,0 +1,9 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.expression; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class PlusExpr implements Expr { } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/TimesExpr.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/TimesExpr.java new file mode 100644 index 0000000000..a5cf1ccc74 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/expression/TimesExpr.java @@ -0,0 +1,9 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.expression; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class TimesExpr implements Expr { } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Circle.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Circle.java new file mode 100644 index 0000000000..c4baabda9b --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Circle.java @@ -0,0 +1,9 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.geometry; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class Circle extends Shape { } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/FilledRectangle.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/FilledRectangle.java new file mode 100644 index 0000000000..92eb290b6b --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/FilledRectangle.java @@ -0,0 +1,10 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.geometry; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class FilledRectangle extends Rectangle { } + diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Rectangle.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Rectangle.java new file mode 100644 index 0000000000..fd66cdd014 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Rectangle.java @@ -0,0 +1,11 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.geometry; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public sealed class Rectangle extends Shape + permits TransparentRectangle, FilledRectangle { } + diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Shape.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Shape.java new file mode 100644 index 0000000000..cdec22ef4c --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Shape.java @@ -0,0 +1,11 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.geometry; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public sealed class Shape + permits Circle, Rectangle, Square { } + diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Square.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Square.java new file mode 100644 index 0000000000..75dafdbc92 --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/Square.java @@ -0,0 +1,10 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.geometry; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public non-sealed class Square extends Shape { } + diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/TransparentRectangle.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/TransparentRectangle.java new file mode 100644 index 0000000000..a5c656157f --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/geometry/TransparentRectangle.java @@ -0,0 +1,10 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package com.example.geometry; + +/** + * @see JEP 360: Sealed Classes (Preview) + */ +public final class TransparentRectangle extends Rectangle { } +