From 4d2853ccc4377aaf8944d02937342f46c7efca43 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 14 Aug 2020 18:46:09 +0200 Subject: [PATCH] [java] Add support for local records (Java 15 Preview) --- pmd-java/etc/grammar/Java.jjt | 28 +++++++++++++------ .../lang/java/ast/ASTRecordDeclaration.java | 4 +++ .../pmd/lang/java/ast/Java15PreviewTest.java | 13 ++++++++- .../jdkversiontests/java15p/LocalRecords.java | 26 +++++++++++++++++ .../ast/jdkversiontests/java15p/Point.java | 4 +++ .../ast/jdkversiontests/java15p/Records.java | 7 +++++ 6 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java diff --git a/pmd-java/etc/grammar/Java.jjt b/pmd-java/etc/grammar/Java.jjt index ff69566461..c9dcdd7fa7 100644 --- a/pmd-java/etc/grammar/Java.jjt +++ b/pmd-java/etc/grammar/Java.jjt @@ -3,6 +3,7 @@ * 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. + * Support Local Records with Java 15 Preview. * Andreas Dangel 08/2020 *==================================================================== * Add support for record types introduced as a preview language @@ -468,11 +469,15 @@ public class JavaParser { } private void checkForRecordType() { - if (jdkVersion != 14 && jdkVersion != 15 || !preview) { + if (!isRecordTypeSupported()) { throwParseException("Records are only supported with Java 14 Preview and Java 15 Preview"); } } + private boolean isRecordTypeSupported() { + return (jdkVersion == 14 || jdkVersion == 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 @@ -1881,22 +1886,25 @@ void BlockStatement(): LOOKAHEAD( { isNextTokenAnAssert() } ) AssertStatement() | LOOKAHEAD( { isYieldStart() } ) YieldStatement() | - LOOKAHEAD(( "final" | Annotation() )* Type() ) + LOOKAHEAD(2147483647, ( "final" | Annotation() )* Type() , {isRecordTypeSupported() && !isKeyword("record") || !isRecordTypeSupported()}) LocalVariableDeclaration() ";" | + LOOKAHEAD({isRecordTypeSupported() && !isKeyword("record") || !isRecordTypeSupported()}) Statement() | // we don't need to lookahead further here // the ambiguity between start of local class and local variable decl - // is already handled in the lookahead guarding LocalVariableDeclaration above. - LocalClassDecl() + // and start of local record and local variable decl or statement + // is already handled in the lookahead guarding LocalVariableDeclaration + // and Statement above. + LocalClassOrRecordDecl() } -void LocalClassDecl() #void: +void LocalClassOrRecordDecl() #void: {int mods = 0;} { - // this preserves the modifiers of the local class. - // it allows for modifiers that are forbidden for local classes, + // 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, @@ -1905,7 +1913,11 @@ void LocalClassDecl() #void: // 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 - mods=Modifiers() ClassOrInterfaceDeclaration(mods) + mods=Modifiers() + ( + LOOKAHEAD({isKeyword("record")}) RecordDeclaration(mods) + | ClassOrInterfaceDeclaration(mods) + ) } /* 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 9c526008c0..f5d98f45da 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,10 @@ public final class ASTRecordDeclaration extends AbstractAnyTypeDeclaration { return true; } + public boolean isLocal() { + return getParent() instanceof ASTBlockStatement; + } + /** * @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 0171d9899c..5874a0ae86 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 @@ -50,6 +50,7 @@ public class Java15PreviewTest { ASTRecordDeclaration recordDecl = compilationUnit.getFirstDescendantOfType(ASTRecordDeclaration.class); Assert.assertEquals("Point", recordDecl.getImage()); Assert.assertFalse(recordDecl.isNested()); + Assert.assertFalse(recordDecl.isLocal()); Assert.assertTrue("Records are implicitly always final", recordDecl.isFinal()); List components = recordDecl.getFirstChildOfType(ASTRecordComponentList.class) .findChildrenOfType(ASTRecordComponent.class); @@ -101,9 +102,10 @@ public class Java15PreviewTest { 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.assertEquals(3, 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.getDeclarations().get(2) instanceof ASTClassOrInterfaceBodyDeclaration); Assert.assertTrue(complex.getParent() instanceof ASTClassOrInterfaceBodyDeclaration); ASTClassOrInterfaceBodyDeclaration complexParent = complex.getFirstParentOfType(ASTClassOrInterfaceBodyDeclaration.class); Assert.assertEquals(DeclarationKind.RECORD, complexParent.getKind()); @@ -153,4 +155,13 @@ public class Java15PreviewTest { public void recordIsARestrictedIdentifier() { java15p.parse("public class record {}"); } + + @Test + public void localRecords() { + ASTCompilationUnit compilationUnit = java15p.parseResource("LocalRecords.java"); + List records = compilationUnit.findDescendantsOfType(ASTRecordDeclaration.class); + Assert.assertEquals(1, records.size()); + Assert.assertEquals("MerchantSales", records.get(0).getSimpleName()); + Assert.assertTrue(records.get(0).isLocal()); + } } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java new file mode 100644 index 0000000000..d958e2b84a --- /dev/null +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java15p/LocalRecords.java @@ -0,0 +1,26 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +import java.util.stream.Collectors; +import java.util.List; + +/** + * @see JEP 384: Records (Second Preview) + */ +public class LocalRecords { + public interface Merchant {} + public static double computeSales(Merchant merchant, int month) { + return month; + } + List findTopMerchants(List merchants, int month) { + // Local record + record MerchantSales(Merchant merchant, double sales) {} + + return merchants.stream() + .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month))) + .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales())) + .map(MerchantSales::merchant) + .collect(Collectors.toList()); + } +} 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 index 231a81c08a..bef547a6c6 100644 --- 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 @@ -1,3 +1,7 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + /** * @see JEP 384: Records (Second Preview) */ 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 index bb7f944e7c..9a756d70c1 100644 --- 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 @@ -1,3 +1,7 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + import java.io.IOException; import java.lang.annotation.Target; import java.lang.annotation.ElementType; @@ -21,7 +25,10 @@ public class Records { this.real = real; this.imaginary = imaginary; } + public record Nested(int a) {} + + public static class NestedClass { } }