diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index 062d5dc597..ff69566461 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -2,6 +2,7 @@ * Remove support for Java 13 preview language features. * Promote text blocks as a permanent language features with Java 15. * Support Pattern Matching for instanceof with Java 15 Preview. + * Support Records with Java 15 Preview. * Andreas Dangel 08/2020 *==================================================================== * Add support for record types introduced as a preview language @@ -419,7 +420,7 @@ public class JavaParser { throwParseException("With JDK 10, 'var' 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, 'record' is a restricted identifier and cannot be used for type declarations!"); + throwParseException("With JDK 14 Preview and JDK 15 Preview, 'record' is a restricted identifier and cannot be used for type declarations!"); } } private void checkForMultipleCaseLabels() { @@ -467,8 +468,8 @@ public class JavaParser { } private void checkForRecordType() { - if (jdkVersion != 14 || !preview) { - throwParseException("Records are only supported with Java 14 Preview"); + if (jdkVersion != 14 && jdkVersion != 15 || !preview) { + throwParseException("Records are only supported with Java 14 Preview and Java 15 Preview"); } } 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 925425f8f5..9c526008c0 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 @@ -57,6 +57,12 @@ public final class ASTRecordDeclaration extends AbstractAnyTypeDeclaration { return isNested(); } + @Override + public boolean isFinal() { + // A record is implicitly final + return true; + } + /** * @deprecated Renamed to {@link #getRecordComponents()} */ 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 9b6086c187..0171d9899c 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 @@ -7,10 +7,12 @@ package net.sourceforge.pmd.lang.java.ast; import java.util.List; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import net.sourceforge.pmd.lang.ast.ParseException; import net.sourceforge.pmd.lang.java.JavaParsingHelper; +import net.sourceforge.pmd.lang.java.ast.ASTAnyTypeBodyDeclaration.DeclarationKind; public class Java15PreviewTest { private final JavaParsingHelper java15p = @@ -42,4 +44,113 @@ public class Java15PreviewTest { java15.parseResource("PatternMatchingInstanceof.java"); } + @Test + public void recordPoint() { + ASTCompilationUnit compilationUnit = java15p.parseResource("Point.java"); + ASTRecordDeclaration recordDecl = compilationUnit.getFirstDescendantOfType(ASTRecordDeclaration.class); + Assert.assertEquals("Point", recordDecl.getImage()); + Assert.assertFalse(recordDecl.isNested()); + Assert.assertTrue("Records are implicitly always final", recordDecl.isFinal()); + List components = recordDecl.getFirstChildOfType(ASTRecordComponentList.class) + .findChildrenOfType(ASTRecordComponent.class); + Assert.assertEquals(2, components.size()); + Assert.assertEquals("x", components.get(0).getVarId().getImage()); + Assert.assertEquals("y", components.get(1).getVarId().getImage()); + Assert.assertNull(components.get(0).getVarId().getNameDeclaration().getAccessNodeParent()); + Assert.assertEquals(Integer.TYPE, components.get(0).getVarId().getNameDeclaration().getType()); + Assert.assertEquals("int", components.get(0).getVarId().getNameDeclaration().getTypeImage()); + } + + @Test(expected = ParseException.class) + public void recordPointBeforeJava15PreviewShouldFail() { + java15.parseResource("Point.java"); + } + + @Test(expected = ParseException.class) + public void recordCtorWithThrowsShouldFail() { + java15p.parse(" record R {" + + " R throws IOException {}" + + " }"); + } + + @Test(expected = ParseException.class) + public void recordMustNotExtend() { + java15p.parse("record RecordEx(int x) extends Number { }"); + } + + @Test(expected = ParseException.class) + @Ignore("Should we check this?") + public void recordCannotBeAbstract() { + java15p.parse("abstract record RecordEx(int x) { }"); + } + + @Test(expected = ParseException.class) + @Ignore("Should we check this?") + public void recordCannotHaveInstanceFields() { + java15p.parse("record RecordFields(int x) { private int y = 1; }"); + } + + @Test + public void innerRecords() { + ASTCompilationUnit compilationUnit = java15p.parseResource("Records.java"); + List recordDecls = compilationUnit.findDescendantsOfType(ASTRecordDeclaration.class, true); + Assert.assertEquals(7, recordDecls.size()); + + ASTRecordDeclaration complex = recordDecls.get(0); + Assert.assertEquals("MyComplex", complex.getSimpleName()); + Assert.assertTrue(complex.isNested()); + Assert.assertEquals(0, getComponent(complex, 0).findChildrenOfType(ASTAnnotation.class).size()); + Assert.assertEquals(1, getComponent(complex, 1).findChildrenOfType(ASTAnnotation.class).size()); + Assert.assertEquals(2, complex.getDeclarations().size()); + Assert.assertTrue(complex.getDeclarations().get(0).getChild(1) instanceof ASTConstructorDeclaration); + Assert.assertTrue(complex.getDeclarations().get(1).getChild(0) instanceof ASTRecordDeclaration); + Assert.assertTrue(complex.getParent() instanceof ASTClassOrInterfaceBodyDeclaration); + ASTClassOrInterfaceBodyDeclaration complexParent = complex.getFirstParentOfType(ASTClassOrInterfaceBodyDeclaration.class); + Assert.assertEquals(DeclarationKind.RECORD, complexParent.getKind()); + Assert.assertSame(complex, complexParent.getDeclarationNode()); + + ASTRecordDeclaration nested = recordDecls.get(1); + Assert.assertEquals("Nested", nested.getSimpleName()); + Assert.assertTrue(nested.isNested()); + + ASTRecordDeclaration range = recordDecls.get(2); + Assert.assertEquals("Range", range.getSimpleName()); + Assert.assertEquals(2, range.getComponentList().size()); + List rangeConstructors = range.findDescendantsOfType(ASTRecordConstructorDeclaration.class); + Assert.assertEquals(1, rangeConstructors.size()); + Assert.assertEquals("Range", rangeConstructors.get(0).getImage()); + Assert.assertTrue(rangeConstructors.get(0).getChild(0) instanceof ASTAnnotation); + Assert.assertEquals(2, range.getDeclarations().size()); + + ASTRecordDeclaration varRec = recordDecls.get(3); + Assert.assertEquals("VarRec", varRec.getSimpleName()); + Assert.assertEquals("x", getComponent(varRec, 0).getVarId().getImage()); + Assert.assertTrue(getComponent(varRec, 0).isVarargs()); + Assert.assertEquals(2, getComponent(varRec, 0).findChildrenOfType(ASTAnnotation.class).size()); + Assert.assertEquals(1, getComponent(varRec, 0).getTypeNode().findDescendantsOfType(ASTAnnotation.class).size()); + + ASTRecordDeclaration arrayRec = recordDecls.get(4); + Assert.assertEquals("ArrayRec", arrayRec.getSimpleName()); + Assert.assertEquals("x", getComponent(arrayRec, 0).getVarId().getImage()); + Assert.assertTrue(getComponent(arrayRec, 0).getVarId().hasArrayType()); + + ASTRecordDeclaration emptyRec = recordDecls.get(5); + Assert.assertEquals("EmptyRec", emptyRec.getSimpleName()); + Assert.assertEquals(0, emptyRec.getComponentList().size()); + + ASTRecordDeclaration personRec = recordDecls.get(6); + Assert.assertEquals("PersonRecord", personRec.getSimpleName()); + ASTImplementsList impl = personRec.getFirstChildOfType(ASTImplementsList.class); + Assert.assertEquals(2, impl.findChildrenOfType(ASTClassOrInterfaceType.class).size()); + } + + private ASTRecordComponent getComponent(ASTRecordDeclaration arrayRec, int index) { + return (ASTRecordComponent) arrayRec.getComponentList().getChild(index); + } + + + @Test(expected = ParseException.class) + public void recordIsARestrictedIdentifier() { + java15p.parse("public class record {}"); + } } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/Point.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/Point.java new file mode 100644 index 0000000000..231a81c08a --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/Point.java @@ -0,0 +1,10 @@ +/** + * @see JEP 384: Records (Second Preview) + */ +public record Point(int x, int y) { + + public static void main(String[] args) { + Point p = new Point(1, 2); + System.out.println("p = " + p); + } +} diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/Records.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/Records.java new file mode 100644 index 0000000000..bb7f944e7c --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/Records.java @@ -0,0 +1,61 @@ +import java.io.IOException; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * @see JEP 384: Records (Second Preview) + */ +public class Records { + + @Target(ElementType.TYPE_USE) + @interface Nullable { } + + @Target({ElementType.CONSTRUCTOR, ElementType.PARAMETER}) + @interface MyAnnotation { } + + public record MyComplex(int real, @Deprecated int imaginary) { + // explicit declaration of a canonical constructor + @MyAnnotation + public MyComplex(@MyAnnotation int real, int imaginary) { + if (real > 100) throw new IllegalArgumentException("too big"); + this.real = real; + this.imaginary = imaginary; + } + public record Nested(int a) {} + } + + + public record Range(int lo, int hi) { + // compact record constructor + @MyAnnotation + public Range { + if (lo > hi) /* referring here to the implicit constructor parameters */ + throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi)); + } + + public void foo() { } + } + + public record VarRec(@Nullable @Deprecated String @Nullable ... x) {} + + public record ArrayRec(int x[]) {} + + public record EmptyRec() { + public void foo() { } + public Type bar() { return null; } + public static void baz() { + EmptyRec r = new EmptyRec<>(); + System.out.println(r); + } + } + + // see https://www.javaspecialists.eu/archive/Issue276.html + public interface Person { + String firstName(); + String lastName(); + } + public record PersonRecord(String firstName, String lastName) + implements Person, java.io.Serializable { + + } +}