[java] Support Unnamed Patterns and Variables for Java 21 Preview

JEP 443
This commit is contained in:
Andreas Dangel
2023-08-05 15:13:49 +02:00
parent 715d58fef3
commit 216dd09405
9 changed files with 871 additions and 19 deletions

View File

@ -1,4 +1,6 @@
/**
* Support "JEP 443: Unnamed Patterns and Variables" for Java 21 Preview.
* New AST nodes: ASTUnnamedPattern
* Support "JEP 430: String Templates" for Java 21 Preview.
* New AST nodes: ASTTemplateExpression, ASTTemplate, ASTTemplateFragment
* Promote "JEP 441: Pattern Matching for switch" as permanent language feature for Java 21.
@ -1884,13 +1886,26 @@ void RecordPattern():
void RecordStructurePattern() #void:
{}
{
"(" [ PatternList() ] ")"
"(" [ ComponentPatternList() ] ")"
}
void PatternList() :
void ComponentPatternList() #PatternList :
{}
{
Pattern() ( "," Pattern() )*
ComponentPattern() ( "," ComponentPattern() )*
}
void ComponentPattern() #void:
{}
{
LOOKAHEAD({isKeyword("_")}) UnnamedPattern()
| Pattern()
}
void UnnamedPattern():
{}
{
softKeyword("_")
}
void InstanceOfExpression() #void:
@ -2637,11 +2652,23 @@ void CaseLabelElement(ASTSwitchLabel label) #void:
{
"null" #NullLiteral [ "," "default" {label.setDefault();} ]
|
LOOKAHEAD(Pattern()) Pattern() [ LOOKAHEAD({isKeyword("when")}) Guard() ]
LOOKAHEAD(Pattern()) CasePattern() [ LOOKAHEAD({isKeyword("when")}) Guard() ]
|
CaseConstant()
}
void CaseConstant() #void:
{}
{
ConditionalExpression()
}
void CasePattern() #void:
{}
{
Pattern()
}
void Guard() :
{}
{

View File

@ -0,0 +1,37 @@
/*
* 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;
/**
* An unnamed pattern, a Java 21 Preview language feature.
*
* <pre class="grammar">
*
* UnnamedPattern ::= "_"
*
* </pre>
*
* @see <a href="https://openjdk.org/jeps/443">JEP 443: Unnamed patterns and variables (Preview)</a> (Java 21)
*/
@Experimental
public final class ASTUnnamedPattern extends AbstractJavaNode implements ASTPattern {
ASTUnnamedPattern(int id) {
super(id);
}
@Override
protected <P, R> R acceptVisitor(JavaVisitor<? super P, ? extends R> visitor, P data) {
return visitor.visit(this, data);
}
@Override
public int getParenthesisDepth() {
return 0;
}
}

View File

@ -45,6 +45,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTType;
import net.sourceforge.pmd.lang.java.ast.ASTTypeArguments;
import net.sourceforge.pmd.lang.java.ast.ASTTypeParameters;
import net.sourceforge.pmd.lang.java.ast.ASTTypePattern;
import net.sourceforge.pmd.lang.java.ast.ASTUnnamedPattern;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.ast.ASTYieldStatement;
import net.sourceforge.pmd.lang.java.ast.JModifier;
@ -167,10 +168,16 @@ public class LanguageLevelChecker<T> {
/**
* String Templates.
* @see <a href="https://openjdk.org/jeps/430">JEP 430: String Templates (Preview)</a>
* @see <a href="https://openjdk.org/jeps/430">JEP 430: String Templates (Preview)</a> (Java 21)
*/
STRING_TEMPLATES(21, 21, false),
/**
* Unnamed patterns and variables.
* @see <a href="https://openjdk.org/jeps/443">JEP 443: Unnamed patterns and variables (Preview)</a> (Java 21)
*/
UNNAMED_PATTERNS_AND_VARIABLES(21, 21, false),
; // SUPPRESS CHECKSTYLE enum trailing semi is awesome
@ -639,6 +646,12 @@ public class LanguageLevelChecker<T> {
return null;
}
@Override
public Void visit(ASTUnnamedPattern node, T data) {
check(node, PreviewFeature.UNNAMED_PATTERNS_AND_VARIABLES, data);
return null;
}
@Override
public Void visitTypeDecl(ASTAnyTypeDeclaration node, T data) {
if (node.getModifiers().hasAnyExplicitly(JModifier.SEALED, JModifier.NON_SEALED)) {
@ -668,7 +681,11 @@ public class LanguageLevelChecker<T> {
} else if ("assert".equals(simpleName)) {
check(node, Keywords.ASSERT_AS_AN_IDENTIFIER, acc);
} else if ("_".equals(simpleName)) {
check(node, Keywords.UNDERSCORE_AS_AN_IDENTIFIER, acc);
if (LanguageLevelChecker.this.preview) {
check(node, PreviewFeature.UNNAMED_PATTERNS_AND_VARIABLES, acc);
} else {
check(node, Keywords.UNDERSCORE_AS_AN_IDENTIFIER, acc);
}
}
}

View File

@ -202,7 +202,9 @@ public final class JavaRuleUtil {
public static boolean isExplicitUnusedVarName(String name) {
return name.startsWith("ignored")
|| name.startsWith("unused")
|| "_".equals(name); // before java 9 it's ok
// before java 9 it's ok, after that, "_" is a reserved keyword
// with Java 21 Preview (JEP 443), "_" means explicitly unused
|| "_".equals(name);
}
/**

View File

@ -4,6 +4,8 @@
package net.sourceforge.pmd.lang.java.ast;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -51,4 +53,18 @@ class Java21PreviewTreeDumpTest extends BaseTreeDumpTest {
JTypeMirror typeMirror = templateExpression.getTypeMirror();
assertEquals("java.lang.String", ((JClassSymbol) typeMirror.getSymbol()).getCanonicalName());
}
@Test
void unnamedPatternsAndVariables() {
doTest("Jep443_UnnamedPatternsAndVariables");
}
@Test
void unnamedPatternsAndVariablesBeforeJava21Preview() {
ParseException thrown = assertThrows(ParseException.class, () -> java21.parseResource("Jep443_UnnamedPatternsAndVariables.java"));
assertThat(thrown.getMessage(), containsString("Since Java 9, '_' is reserved and cannot be used as an identifier"));
thrown = assertThrows(ParseException.class, () -> java21.parseResource("Jep443_UnnamedPatternsAndVariables2.java"));
assertThat(thrown.getMessage(), containsString("Unnamed patterns and variables is a preview feature of JDK 21, you should select your language version accordingly"));
}
}

View File

@ -4,8 +4,9 @@
package net.sourceforge.pmd.lang.java.ast;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
@ -38,7 +39,7 @@ class Java21TreeDumpTest extends BaseTreeDumpTest {
@Test
void patternMatchingForSwitchBeforeJava21() {
ParseException thrown = assertThrows(ParseException.class, () -> java20.parseResource("Jep441_PatternMatchingForSwitch.java"));
assertTrue(thrown.getMessage().contains("Patterns in switch statements is a preview feature of JDK 20, you should select your language version accordingly"));
assertThat(thrown.getMessage(), containsString("Patterns in switch statements is a preview feature of JDK 20, you should select your language version accordingly"));
}
@Test
@ -49,8 +50,7 @@ class Java21TreeDumpTest extends BaseTreeDumpTest {
@Test
void dealingWithNullBeforeJava21() {
ParseException thrown = assertThrows(ParseException.class, () -> java20.parseResource("DealingWithNull.java"));
assertTrue(thrown.getMessage().contains("Null in switch cases is a preview feature of JDK 20, you should select your language version accordingly"),
"Unexpected message: " + thrown.getMessage());
assertThat(thrown.getMessage(), containsString("Null in switch cases is a preview feature of JDK 20, you should select your language version accordingly"));
}
@ -72,8 +72,7 @@ class Java21TreeDumpTest extends BaseTreeDumpTest {
@Test
void guardedPatternsBeforeJava21() {
ParseException thrown = assertThrows(ParseException.class, () -> java20.parseResource("GuardedPatterns.java"));
assertTrue(thrown.getMessage().contains("Patterns in switch statements is a preview feature of JDK 20, you should select your language version accordingly"),
"Unexpected message: " + thrown.getMessage());
assertThat(thrown.getMessage(), containsString("Patterns in switch statements is a preview feature of JDK 20, you should select your language version accordingly"));
}
@Test
@ -84,8 +83,7 @@ class Java21TreeDumpTest extends BaseTreeDumpTest {
@Test
void patternsInSwitchLabelsBeforeJava21() {
ParseException thrown = assertThrows(ParseException.class, () -> java20.parseResource("PatternsInSwitchLabels.java"));
assertTrue(thrown.getMessage().contains("Patterns in switch statements is a preview feature of JDK 20, you should select your language version accordingly"),
"Unexpected message: " + thrown.getMessage());
assertThat(thrown.getMessage(), containsString("Patterns in switch statements is a preview feature of JDK 20, you should select your language version accordingly"));
}
@Test
@ -106,8 +104,7 @@ class Java21TreeDumpTest extends BaseTreeDumpTest {
@Test
void recordPatternsJepBeforeJava21() {
ParseException thrown = assertThrows(ParseException.class, () -> java20.parseResource("Jep440_RecordPatterns.java"));
assertTrue(thrown.getMessage().contains("Record patterns is a preview feature of JDK 20, you should select your language version accordingly"),
"Unexpected message: " + thrown.getMessage());
assertThat(thrown.getMessage(), containsString("Record patterns is a preview feature of JDK 20, you should select your language version accordingly"));
}
@Test
@ -118,8 +115,7 @@ class Java21TreeDumpTest extends BaseTreeDumpTest {
@Test
void recordPatternsBeforeJava21() {
ParseException thrown = assertThrows(ParseException.class, () -> java20.parseResource("RecordPatterns.java"));
assertTrue(thrown.getMessage().contains("Record patterns is a preview feature of JDK 20, you should select your language version accordingly"),
"Unexpected message: " + thrown.getMessage());
assertThat(thrown.getMessage(), containsString("Record patterns is a preview feature of JDK 20, you should select your language version accordingly"));
}
@Test

View File

@ -0,0 +1,123 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import java.util.stream.Collectors;
/**
* @see <a href="https://openjdk.org/jeps/443">JEP 443: Unnamed Patterns and Variables (Preview)</a>
*/
class Jep443_UnamedPatternsAndVariables {
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }
void unnamedPatterns1() {
ColoredPoint r = new ColoredPoint(new Point(3,4), Color.GREEN);
if (r instanceof ColoredPoint(Point p, Color _)) {
System.out.println(p.x() + " " + p.y());
}
if (r instanceof ColoredPoint(Point(int x, int y), _)) {
System.out.println(x + " " + y);
}
}
sealed abstract class Ball permits RedBall, BlueBall, GreenBall { }
final class RedBall extends Ball { }
final class BlueBall extends Ball { }
final class GreenBall extends Ball { }
record Box<T extends Ball>(T content) { }
void unnamedPatterns2() {
Box<? extends Ball> b = new Box<>(new RedBall());
switch (b) {
case Box(RedBall _) -> processBox(b);
case Box(BlueBall _) -> processBox(b);
case Box(GreenBall _) -> stopProcessing();
}
switch (b) {
case Box(RedBall _), Box(BlueBall _) -> processBox(b);
case Box(GreenBall _) -> stopProcessing();
case Box(_) -> pickAnotherBox();
}
int x = 42;
switch (b) {
// multiple patterns guarded by one guard
case Box(RedBall _), Box(BlueBall _) when x == 42 -> processBox(b);
case Box(_) -> pickAnotherBox();
}
}
private void processBox(Box<? extends Ball> b) {}
private void stopProcessing() {}
private void pickAnotherBox() {}
class Order {}
private static final int LIMIT = 10;
private int sideEffect() {
return 0;
}
void unnamedVariables(List<Order> orders) {
int total = 0;
for (Order _ : orders) {
if (total < LIMIT) {
total++;
}
}
System.out.println("total: " + total);
for (int i = 0, _ = sideEffect(); i < 10; i++) {
System.out.println(i);
}
Queue<Integer> q = new ArrayDeque<>(); // x1, y1, z1, x2, y2, z2 ..
while (q.size() >= 3) {
int x = q.remove();
int y = q.remove();
int _ = q.remove(); // z is unused
Point p = new Point(x, y);
}
while (q.size() >= 3) {
var x = q.remove();
var _ = q.remove();
var _ = q.remove();
Point p = new Point(x, 0);
}
}
static class ScopedContext implements AutoCloseable {
@Override
public void close() { }
public static ScopedContext acquire() {
return new ScopedContext();
}
}
void unusedVariables2() {
try (var _ = ScopedContext.acquire()) {
//... acquiredContext not used ...
}
String s = "123";
try {
int i = Integer.parseInt(s);
System.out.println(i);
} catch (NumberFormatException _) {
System.out.println("Bad number: " + s);
} catch (Exception _) {
System.out.println("error...");
}
List.of("a", "b").stream().collect(Collectors.toMap(String::toUpperCase, _ -> "NO_DATA"));
}
}

View File

@ -0,0 +1,26 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import java.util.stream.Collectors;
/**
* @see <a href="https://openjdk.org/jeps/443">JEP 443: Unnamed Patterns and Variables (Preview)</a>
*/
class Jep443_UnamedPatternsAndVariables2 {
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }
void unnamedPatterns1() {
ColoredPoint r = new ColoredPoint(new Point(3,4), Color.GREEN);
if (r instanceof ColoredPoint(Point(int x, int y), _)) {
System.out.println(x + " " + y);
}
}
}