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 { }
+