[java] Add support for RecordPatterns

See JEP 405
This commit is contained in:
Andreas Dangel
2022-07-07 15:06:52 +02:00
parent eeeccde494
commit a5b68ddfcf
11 changed files with 739 additions and 14 deletions

View File

@ -2,6 +2,7 @@
* Support "JEP 427: Pattern Matching for switch (Third Preview)" for Java 19 Preview
* Note: GuardedPattern is deprecated and only valid for 17-preview and 18-preview
* New AST node: Guard - used within switch case labels for refining a pattern
* Support "JEP 405: Record Patterns (Preview)" for Java 19 Preview
* Andreas Dangel 07/2022
*====================================================================
* Support "JEP 420: Pattern Matching for switch (Second Preview)" for Java 18 Preview
@ -578,6 +579,12 @@ public class JavaParser {
}
}
private void checkForRecordPatterns() {
if (!((jdkVersion == 19) && preview)) {
throwParseException("Record Patterns are only supported with JDK 19 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
@ -1778,8 +1785,9 @@ void EqualityExpression() #EqualityExpression(>1):
void Pattern() #void:
{}
{
TypePattern() [ GuardedPatternCondition() #GuardedPattern(2) {checkForGuardedPatterns();} ]
| ParenthesizedPattern()
LOOKAHEAD(ReferenceType() "(") RecordPattern()
| LOOKAHEAD("(") ParenthesizedPattern()
| TypePattern() [ GuardedPatternCondition() #GuardedPattern(2) {checkForGuardedPatterns();} ]
}
void GuardedPatternCondition() #void:
@ -1802,6 +1810,24 @@ void TypePattern():
VariableDeclaratorId()
}
void RecordPattern():
{ checkForRecordPatterns(); }
{
ReferenceType() RecordStructurePattern() [ VariableDeclaratorId() ]
}
void RecordStructurePattern():
{}
{
"(" [ RecordComponentPatternList() ] ")"
}
void RecordComponentPatternList() #void:
{}
{
Pattern() ( "," Pattern() )*
}
void InstanceOfExpression() #InstanceOfExpression(>1):
{}
{
@ -1811,6 +1837,8 @@ void InstanceOfExpression() #InstanceOfExpression(>1):
LOOKAHEAD("final" | "@") {checkforBadInstanceOfPattern();} Pattern()
|
LOOKAHEAD("(") Pattern() {checkForParenthesizedInstanceOfPattern();}
|
LOOKAHEAD(ReferenceType() "(") RecordPattern()
|
Type()
[ {checkforBadInstanceOfPattern();} VariableDeclaratorId() #TypePattern(2) ]

View File

@ -10,17 +10,16 @@ import net.sourceforge.pmd.annotation.Experimental;
* A pattern (for pattern matching constructs like {@link ASTInstanceOfExpression InstanceOfExpression}
* or within a {@link ASTSwitchLabel}). This is a JDK 16 feature.
*
* <p>This interface will be implemented by all forms of patterns. For
* now, only type test patterns are supported. Record deconstruction
* patterns is planned for a future JDK version.
* <p>This interface will be implemented by all forms of patterns.
*
* <pre class="grammar">
*
* Pattern ::= {@link ASTTypePattern TypePattern}
* Pattern ::= {@linkplain ASTTypePattern TypePattern} | {@linkplain ASTRecordPattern RecordPattern}
*
* </pre>
*
* @see <a href="https://openjdk.java.net/jeps/394">JEP 394: Pattern Matching for instanceof</a>
* @see <a href="https://openjdk.org/jeps/394">JEP 394: Pattern Matching for instanceof</a>
* @see <a href="https://openjdk.org/jeps/405">JEP 405: Record Patterns (Preview)</a>
*/
public interface ASTPattern extends JavaNode {

View File

@ -0,0 +1,61 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.ast;
import net.sourceforge.pmd.annotation.Experimental;
/**
* A record pattern (JDK19). This can be found on
* the right-hand side of an {@link ASTInstanceOfExpression InstanceOfExpression}.
*
* <pre class="grammar">
*
* RecordPattern ::= {@linkplain ASTReferenceType ReferenceType} {@linkplain ASTRecordStructurePattern RecordStructurePattern} [ {@linkplain ASTVariableDeclaratorId} VariableDeclaratorId ]
*
* </pre>
*
* @see <a href="https://openjdk.org/jeps/405">JEP 405: Record Patterns (Preview)</a>
*/
@Experimental
public final class ASTRecordPattern extends AbstractJavaNode implements ASTPattern {
private int parenDepth;
ASTRecordPattern(int id) {
super(id);
}
ASTRecordPattern(JavaParser p, int id) {
super(p, id);
}
@Override
public Object jjtAccept(JavaParserVisitor visitor, Object data) {
return visitor.visit(this, data);
}
/**
* Gets the type against which the expression is tested.
*/
public ASTReferenceType getTypeNode() {
return getFirstChildOfType(ASTReferenceType.class);
}
/** Returns the declared variable. */
public ASTVariableDeclaratorId getVarId() {
return getFirstChildOfType(ASTVariableDeclaratorId.class);
}
void bumpParenDepth() {
parenDepth++;
}
@Override
@Experimental
public int getParenthesisDepth() {
return parenDepth;
}
}

View File

@ -0,0 +1,35 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.ast;
import net.sourceforge.pmd.annotation.Experimental;
/**
* Contains a potentially empty list of nested Patterns for {@linkplain ASTRecordPattern RecordPattern} (JDK 19).
*
* <pre class="grammar">
*
* RecordStructurePattern ::= "(" {@linkplain ASTPattern Pattern} ( "," {@linkplain ASTPattern pattern} ) ")"
*
* </pre>
*
* @see <a href="https://openjdk.org/jeps/405">JEP 405: Record Patterns (Preview)</a>
*/
@Experimental
public final class ASTRecordStructurePattern extends AbstractJavaNode {
ASTRecordStructurePattern(int id) {
super(id);
}
ASTRecordStructurePattern(JavaParser p, int id) {
super(p, id);
}
@Override
public Object jjtAccept(JavaParserVisitor visitor, Object data) {
return visitor.visit(this, data);
}
}

View File

@ -957,4 +957,18 @@ public class JavaParserDecoratedVisitor implements JavaParserVisitor {
visitor.visit(node, data);
return visit((JavaNode) node, data);
}
@Experimental
@Override
public Object visit(ASTRecordPattern node, Object data) {
visitor.visit(node, data);
return visit((JavaNode) node, data);
}
@Experimental
@Override
public Object visit(ASTRecordStructurePattern node, Object data) {
visitor.visit(node, data);
return visit((JavaNode) node, data);
}
}

View File

@ -675,4 +675,16 @@ public class JavaParserVisitorAdapter implements JavaParserVisitor {
public Object visit(ASTGuard node, Object data) {
return visit((JavaNode) node, data);
}
@Experimental
@Override
public Object visit(ASTRecordPattern node, Object data) {
return visit((JavaNode) node, data);
}
@Experimental
@Override
public Object visit(ASTRecordStructurePattern node, Object data) {
return visit((JavaNode) node, data);
}
}

View File

@ -807,4 +807,16 @@ public class JavaParserVisitorDecorator implements JavaParserControllessVisitor
public Object visit(ASTGuard node, Object data) {
return visitor.visit(node, data);
}
@Experimental
@Override
public Object visit(ASTRecordPattern node, Object data) {
return visitor.visit(node, data);
}
@Experimental
@Override
public Object visit(ASTRecordStructurePattern node, Object data) {
return visitor.visit(node, data);
}
}

View File

@ -104,6 +104,8 @@ import net.sourceforge.pmd.lang.java.ast.ASTRecordBody;
import net.sourceforge.pmd.lang.java.ast.ASTRecordComponent;
import net.sourceforge.pmd.lang.java.ast.ASTRecordComponentList;
import net.sourceforge.pmd.lang.java.ast.ASTRecordDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTRecordPattern;
import net.sourceforge.pmd.lang.java.ast.ASTRecordStructurePattern;
import net.sourceforge.pmd.lang.java.ast.ASTReferenceType;
import net.sourceforge.pmd.lang.java.ast.ASTRelationalExpression;
import net.sourceforge.pmd.lang.java.ast.ASTResource;
@ -889,5 +891,17 @@ public abstract class AbstractJavaRule extends AbstractRule implements JavaParse
return visit((JavaNode) node, data);
}
@Experimental
@Override
public Object visit(ASTRecordPattern node, Object data) {
return visit((JavaNode) node, data);
}
@Experimental
@Override
public Object visit(ASTRecordStructurePattern node, Object data) {
return visit((JavaNode) node, data);
}
// CPD-ON
}

View File

@ -4,7 +4,9 @@
package net.sourceforge.pmd.lang.java.ast;
import org.junit.Assert;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
@ -31,13 +33,13 @@ public class Java19PreviewTreeDumpTest extends BaseTreeDumpTest {
@Test
public void dealingWithNullBeforeJava19Preview() {
ParseException thrown = Assert.assertThrows(ParseException.class, new ThrowingRunnable() {
ParseException thrown = assertThrows(ParseException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
java19.parseResource("DealingWithNull.java");
}
});
Assert.assertTrue("Unexpected message: " + thrown.getMessage(),
assertTrue("Unexpected message: " + thrown.getMessage(),
thrown.getMessage().contains("Null case labels in switch are only supported with JDK 17 Preview or JDK 18 Preview or JDK 19 Preview."));
}
@ -58,13 +60,13 @@ public class Java19PreviewTreeDumpTest extends BaseTreeDumpTest {
@Test
public void guardedAndParenthesizedPatternsBeforeJava19Preview() {
ParseException thrown = Assert.assertThrows(ParseException.class, new ThrowingRunnable() {
ParseException thrown = assertThrows(ParseException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
java19.parseResource("GuardedAndParenthesizedPatterns.java");
}
});
Assert.assertTrue("Unexpected message: " + thrown.getMessage(),
assertTrue("Unexpected message: " + thrown.getMessage(),
thrown.getMessage().contains("Pattern Matching in Switch is only supported with JDK 17 Preview or JDK 18 Preview or JDK 19 Preview."));
}
@ -75,13 +77,13 @@ public class Java19PreviewTreeDumpTest extends BaseTreeDumpTest {
@Test
public void patternsInSwitchLabelsBeforeJava19Preview() {
ParseException thrown = Assert.assertThrows(ParseException.class, new ThrowingRunnable() {
ParseException thrown = assertThrows(ParseException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
java19.parseResource("PatternsInSwitchLabels.java");
}
});
Assert.assertTrue("Unexpected message: " + thrown.getMessage(),
assertTrue("Unexpected message: " + thrown.getMessage(),
thrown.getMessage().contains("Pattern Matching in Switch is only supported with JDK 17 Preview or JDK 18 Preview or JDK 19 Preview."));
}
@ -99,4 +101,21 @@ public class Java19PreviewTreeDumpTest extends BaseTreeDumpTest {
public void scopeOfPatternVariableDeclarations() {
doTest("ScopeOfPatternVariableDeclarations");
}
@Test
public void recordPatterns() {
doTest("RecordPatterns");
}
@Test
public void recordPatternsBeforeJava19Preview() {
ParseException thrown = assertThrows(ParseException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
java19.parseResource("RecordPatterns.java");
}
});
assertTrue("Unexpected message: " + thrown.getMessage(),
thrown.getMessage().contains("Record Patterns are only supported with JDK 19 Preview."));
}
}

View File

@ -0,0 +1,64 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
/**
* @see <a href="https://openjdk.org/jeps/405">JEP 405: Record Patterns (Preview)</a>
*/
public class RecordPatterns {
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
void printSum1(Object o) {
if (o instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
// record pattern
void printSum2(Object o) {
if (o instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
void printUpperLeftColoredPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.println(ul.c());
}
}
// nested record pattern
void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
// fully nested record pattern, also using "var"
void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
var lr)) {
System.out.println("Upper-left corner: " + x);
}
}
// record patterns with generic types
record Box<T>(T t) {}
void test1(Box<Object> bo) {
if (bo instanceof Box<Object>(String s)) {
System.out.println("String " + s);
}
}
void test2(Box<String> bo) {
if (bo instanceof Box<String>(var s)) {
System.out.println("String " + s);
}
}
}