From 18b9ec2d90dc4cbcac75c895932ed657ba0b9a1d Mon Sep 17 00:00:00 2001 From: Suzanne Millstein Date: Mon, 2 Oct 2023 08:01:26 -0700 Subject: [PATCH] Implement new Java 21 features in dataflow (#6192) Co-authored-by: Michael Ernst --- build.gradle | 6 + .../checker/fenum/FenumVisitor.java | 2 +- .../checker/nullness/NullnessVisitor.java | 18 +- checker/tests/nullness/java17/Issue5967.java | 23 ++ .../nullness/java17/SwitchTestIssue5412.java | 2 +- checker/tests/nullness/java21/FlowSwitch.java | 98 ++++++++ .../nullness/java21/SimpleCaseGuard.java | 31 +++ .../cfg/builder/CFGTranslationPhaseOne.java | 131 ++++++---- .../cfg/node/AbstractNodeVisitor.java | 5 + .../dataflow/cfg/node/CaseNode.java | 20 +- .../cfg/node/DeconstructorPatternNode.java | 94 +++++++ .../dataflow/cfg/node/InstanceOfNode.java | 54 +++- .../dataflow/cfg/node/NodeVisitor.java | 9 + docs/CHANGELOG.md | 12 +- .../CheckerFrameworkPerDirectoryTest.java | 4 + .../framework/test/TestUtilities.java | 6 +- .../common/aliasing/AliasingVisitor.java | 21 +- .../common/basetype/BaseTypeVisitor.java | 37 ++- .../framework/ajava/ExpectedTreesVisitor.java | 4 +- .../ajava/JointJavacJavaParserVisitor.java | 10 +- .../ajava/TreeScannerWithDefaults.java | 19 +- .../framework/flow/CFAbstractTransfer.java | 20 +- .../framework/type/TypeFromMemberVisitor.java | 12 +- .../tests/all-systems/java21/Issue6173.java | 26 ++ .../tests/all-systems/java21/JEP440.java | 76 ++++++ .../tests/all-systems/java21/JEP441.java | 230 ++++++++++++++++++ .../javacutil/SwitchExpressionScanner.java | 6 +- .../checkerframework/javacutil/TreeUtils.java | 47 +++- .../{trees => }/TreeUtilsAfterJava11.java | 72 +++++- 29 files changed, 947 insertions(+), 148 deletions(-) create mode 100644 checker/tests/nullness/java17/Issue5967.java create mode 100644 checker/tests/nullness/java21/FlowSwitch.java create mode 100644 checker/tests/nullness/java21/SimpleCaseGuard.java create mode 100644 dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java create mode 100644 framework/tests/all-systems/java21/Issue6173.java create mode 100644 framework/tests/all-systems/java21/JEP440.java create mode 100644 framework/tests/all-systems/java21/JEP441.java rename javacutil/src/main/java/org/checkerframework/javacutil/{trees => }/TreeUtilsAfterJava11.java (86%) diff --git a/build.gradle b/build.gradle index 0b846f58031..29c21dfdfca 100644 --- a/build.gradle +++ b/build.gradle @@ -201,6 +201,12 @@ allprojects { doNotFormat += ['**/java17/', '**/*record*/'] } + // As of 2023-09-24, google-java-format cannot parse Java 21 language features. + // See https://github.com/google/google-java-format/releases. + if (true) { + doNotFormat += ['**/java21/'] + } + if (!isJava21orHigher) { doNotFormat += ['**/java21/'] } diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java index 96f729f322a..0a7580c77f3 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java @@ -15,7 +15,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.CaseUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; /** The visitor for Fenum Checker. */ public class FenumVisitor extends BaseTypeVisitor { diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java index 1a69afe4375..45039f315bd 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java @@ -419,14 +419,16 @@ public Void visitIf(IfTree tree, Void p) { public Void visitInstanceOf(InstanceOfTree tree, Void p) { // The "reference type" is the type after "instanceof". Tree refTypeTree = tree.getType(); - if (refTypeTree.getKind() == Tree.Kind.ANNOTATED_TYPE) { - List annotations = - TreeUtils.annotationsFromTree((AnnotatedTypeTree) refTypeTree); - if (AnnotationUtils.containsSame(annotations, NULLABLE)) { - checker.reportError(tree, "instanceof.nullable"); - } - if (AnnotationUtils.containsSame(annotations, NONNULL)) { - checker.reportWarning(tree, "instanceof.nonnull.redundant"); + if (refTypeTree != null) { + if (refTypeTree.getKind() == Tree.Kind.ANNOTATED_TYPE) { + List annotations = + TreeUtils.annotationsFromTree((AnnotatedTypeTree) refTypeTree); + if (AnnotationUtils.containsSame(annotations, NULLABLE)) { + checker.reportError(tree, "instanceof.nullable"); + } + if (AnnotationUtils.containsSame(annotations, NONNULL)) { + checker.reportWarning(tree, "instanceof.nonnull.redundant"); + } } } // Don't call super because it will issue an incorrect instanceof.unsafe warning. diff --git a/checker/tests/nullness/java17/Issue5967.java b/checker/tests/nullness/java17/Issue5967.java new file mode 100644 index 00000000000..16f05811ae0 --- /dev/null +++ b/checker/tests/nullness/java17/Issue5967.java @@ -0,0 +1,23 @@ +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.NonNull; + +// @below-java17-jdk-skip-test +public final class Issue5967 { + + enum TestEnum { + FIRST, + SECOND; + } + + public static void main(String[] args) { + TestEnum testEnum = TestEnum.FIRST; + Supplier supplier = + switch (testEnum) { + case FIRST: + yield () -> 1; + case SECOND: + yield () -> 2; + }; + @NonNull Supplier supplier1 = supplier; + } +} diff --git a/checker/tests/nullness/java17/SwitchTestIssue5412.java b/checker/tests/nullness/java17/SwitchTestIssue5412.java index e7efc937ea5..4d1bdc20cc6 100644 --- a/checker/tests/nullness/java17/SwitchTestIssue5412.java +++ b/checker/tests/nullness/java17/SwitchTestIssue5412.java @@ -30,9 +30,9 @@ public String foo1a(MyEnum b) { // The default case is dead code, so it would be possible for type-checking // to skip it and not issue this warning. But giving the warning is also // good. - // :: error: (switch.expression) default -> null; }; + // :: error: (return) return s; } diff --git a/checker/tests/nullness/java21/FlowSwitch.java b/checker/tests/nullness/java21/FlowSwitch.java new file mode 100644 index 00000000000..e103357346f --- /dev/null +++ b/checker/tests/nullness/java21/FlowSwitch.java @@ -0,0 +1,98 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +public class FlowSwitch { + + void test0(Number n) { + String s = null; + switch (n) { + case null, default: { + // TODO: this should issue a dereference of nullable error. + n.toString(); + s = ""; + } + } + s.toString(); + } + + void test1(Integer i) { + String msg = null; + switch (i) { + case -1, 1: + msg = "-1 or 1"; + break; + case Integer j when j > 0: + msg = "pos"; + break; + case Integer j: + msg = "everything else"; + break; + } + msg.toString(); + } + + void test2(Integer i) { + String msg = null; + switch (i) { + case -1, 1: + msg = "-1 or 1"; + break; + default: + msg = "everythingything else"; + break; + case 2: + msg = "pos"; + break; + } + msg.toString(); + } + + class A {} + + class B extends A {} + + sealed interface I permits C, D {} + + final class C implements I {} + + final class D implements I {} + + record Pair(T x, T y) {} + + void testE(Pair p1) { + B e = switch (p1) { + case Pair(A a, B b) -> b; + case Pair(B b, A a) -> b; + default -> null; + }; + B e2 = null; + switch (p1) { + case Pair(A a, B b) -> e2 = b; + case Pair(B b, A a) -> e2 = b; + default -> e2 = new B(); + } + e2.toString(); + } + + void test3(Pair p2) { + String s = null; + I e = null; + switch (p2) { + case Pair(I i, C c) -> {e = c; s="";} + case Pair(I i, D d) -> {e = d; s="";} + } + s.toString(); + e.toString(); + + I e2 = null; + switch (p2) { + case Pair(C c, I i) -> e2 = c; + case Pair(D d, C c) -> e2 = d; + case Pair(D d1, D d2) -> e2 = d2; + } + } +} diff --git a/checker/tests/nullness/java21/SimpleCaseGuard.java b/checker/tests/nullness/java21/SimpleCaseGuard.java new file mode 100644 index 00000000000..805c93d5b91 --- /dev/null +++ b/checker/tests/nullness/java21/SimpleCaseGuard.java @@ -0,0 +1,31 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class SimpleCaseGuard { + + @Nullable String field; + + void test2(Object obj, boolean b) { + switch (obj) { + case String s when field != null -> { + @NonNull String z = field; + } + case String s -> { + // :: error: (assignment) + @NonNull String z = field; + } + default -> { + // :: error: (assignment) + @NonNull String z = field; + } + } + } + +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 79f6a593202..c8665ea8d8a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -46,6 +46,7 @@ import com.sun.source.tree.SynchronizedTree; import com.sun.source.tree.ThrowTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TryTree; import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.TypeParameterTree; @@ -104,6 +105,7 @@ import org.checkerframework.dataflow.cfg.node.ConditionalAndNode; import org.checkerframework.dataflow.cfg.node.ConditionalNotNode; import org.checkerframework.dataflow.cfg.node.ConditionalOrNode; +import org.checkerframework.dataflow.cfg.node.DeconstructorPatternNode; import org.checkerframework.dataflow.cfg.node.DoubleLiteralNode; import org.checkerframework.dataflow.cfg.node.EqualToNode; import org.checkerframework.dataflow.cfg.node.ExplicitThisNode; @@ -166,15 +168,16 @@ import org.checkerframework.javacutil.SystemUtil; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.DeconstructionPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.InstanceOfUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.YieldUtils; import org.checkerframework.javacutil.TypeAnnotationUtils; import org.checkerframework.javacutil.TypeKindUtils; import org.checkerframework.javacutil.TypesUtils; import org.checkerframework.javacutil.trees.TreeBuilder; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.BindingPatternUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.CaseUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.InstanceOfUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.SwitchExpressionUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.YieldUtils; import org.plumelib.util.ArrayMap; import org.plumelib.util.ArraySet; import org.plumelib.util.CollectionsPlume; @@ -553,6 +556,8 @@ public Node scan(Tree tree, Void p) { return visitSwitchExpression17(tree, p); case "YIELD": return visitYield17(tree, p); + case "DECONSTRUCTION_PATTERN": + return visitDeconstructionPattern21(tree, p); default: // fall through to generic behavior } @@ -611,6 +616,26 @@ public Node visitBindingPattern17(Tree bindingPatternTree, Void p) { return varNode; } + /** + * Visit a DeconstructionPatternTree. + * + * @param deconstructionPatternTree a DeconstructionPatternTree, typed as Tree so the Checker + * Framework compiles under JDK 20 and earlier + * @param p an unused parameter + * @return the result of visiting the tree + */ + public Node visitDeconstructionPattern21(Tree deconstructionPatternTree, Void p) { + List nestedPatternTrees = + DeconstructionPatternUtils.getNestedPatterns(deconstructionPatternTree); + List nestedPatterns = new ArrayList<>(nestedPatternTrees.size()); + for (Tree pattern : nestedPatternTrees) { + nestedPatterns.add(scan(pattern, p)); + } + + return new DeconstructorPatternNode( + TreeUtils.typeOf(deconstructionPatternTree), deconstructionPatternTree, nestedPatterns); + } + /* --------------------------------------------------------- */ /* Nodes and Labels Management */ /* --------------------------------------------------------- */ @@ -2339,15 +2364,11 @@ private SwitchBuilder(Tree switchTree) { // before the default case, no matter where `default:` is written. Therefore, // build the default case last. defaultIndex = i; + } else if (i == numCases - 1 && defaultIndex == -1) { + // This is the last case, and it's not a default case. + buildCase(caseTree, i, exhaustiveIgnoreDefault()); } else { - boolean isLastCaseExceptDefault = - i == numCases - 1 - || (i == numCases - 2 && TreeUtils.isDefaultCaseTree(caseTrees.get(i + 1))); - // This can be extended to handle case statements as well as case rules. - boolean noFallthroughToHere = CaseUtils.isCaseRule(caseTree); - boolean isLastCaseOfExhaustive = - isLastCaseExceptDefault && casesAreExhaustive() && noFallthroughToHere; - buildCase(caseTree, i, isLastCaseOfExhaustive); + buildCase(caseTree, i, false); } } @@ -2466,10 +2487,15 @@ private void buildCase(CaseTree caseTree, int index, boolean isLastCaseOfExhaust if (!isTerminalCase) { // A case expression exists, and it needs to be tested. ArrayList exprs = new ArrayList<>(); - for (ExpressionTree exprTree : CaseUtils.getExpressions(caseTree)) { + for (Tree exprTree : CaseUtils.getLabels(caseTree)) { exprs.add(scan(exprTree, null)); } - CaseNode test = new CaseNode(caseTree, selectorExprAssignment, exprs, env.getTypeUtils()); + + ExpressionTree guardTree = CaseUtils.getGuard(caseTree); + Node guard = (guardTree == null) ? null : scan(guardTree, null); + + CaseNode test = + new CaseNode(caseTree, selectorExprAssignment, exprs, guard, env.getTypeUtils()); extendWithNode(test); extendWithExtendedNode(new ConditionalJump(thisBodyLabel, nextCaseLabel)); } @@ -2545,39 +2571,44 @@ private void buildCase(CaseTree caseTree, int index, boolean isLastCaseOfExhaust } /** - * Returns true if the cases are exhaustive -- exactly one is executed. There might or might not - * be a `default` case label; if there is, it is never used. + * Returns true if the switch is exhaustive; ignoring any default case * - * @return true if the cases are exhaustive + * @return true if the switch is exhaustive; ignoring any default case */ - private boolean casesAreExhaustive() { - TypeMirror selectorTypeMirror = TreeUtils.typeOf(selectorExprTree); + private boolean exhaustiveIgnoreDefault() { + // Switch expressions are always exhaustive, but they might have a default case, which is why + // the above loop is not fused with the below loop. + if (!TreeUtils.isSwitchStatement(switchTree)) { + return true; + } - switch (selectorTypeMirror.getKind()) { - case BOOLEAN: - // TODO - break; - case DECLARED: - DeclaredType declaredType = (DeclaredType) selectorTypeMirror; - TypeElement declaredTypeElement = (TypeElement) declaredType.asElement(); - if (declaredTypeElement.getKind() == ElementKind.ENUM) { - // It's an enumerated type. - List enumConstants = - ElementUtils.getEnumConstants(declaredTypeElement); - List caseLabels = new ArrayList<>(enumConstants.size()); - for (CaseTree caseTree : caseTrees) { - for (ExpressionTree caseEnumConstant : CaseUtils.getExpressions(caseTree)) { - caseLabels.add(((IdentifierTree) caseEnumConstant).getName()); - } - } - // Could also check that the values match. - boolean result = enumConstants.size() == caseLabels.size(); - return result; + int enumCaseLabels = 0; + for (CaseTree caseTree : caseTrees) { + for (Tree caseLabel : CaseUtils.getLabels(caseTree)) { + // Java guarantees that if one of the cases is the null literal, the switch is exhaustive. + // Also if certain other constructs exist. + if (caseLabel.getKind() == Kind.NULL_LITERAL + || TreeUtils.isBindingPatternTree(caseLabel) + || TreeUtils.isDeconstructionPatternTree(caseLabel)) { + return true; } - break; - default: - break; + if (caseLabel.getKind() == Kind.IDENTIFIER) { + enumCaseLabels++; + } + } + } + + TypeMirror selectorTypeMirror = TreeUtils.typeOf(selectorExprTree); + if (selectorTypeMirror.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) selectorTypeMirror; + TypeElement declaredTypeElement = (TypeElement) declaredType.asElement(); + if (declaredTypeElement.getKind() == ElementKind.ENUM) { + // The switch expression's type is an enumerated type. + List enumConstants = ElementUtils.getEnumConstants(declaredTypeElement); + return enumConstants.size() == enumCaseLabels; + } } + return false; } } @@ -3803,12 +3834,14 @@ public Node visitTypeParameter(TypeParameterTree tree, Void p) { public Node visitInstanceOf(InstanceOfTree tree, Void p) { InstanceOfNode instanceOfNode; Node operand = scan(tree.getExpression(), p); - TypeMirror refType = TreeUtils.typeOf(tree.getType()); - Tree binding = InstanceOfUtils.getPattern(tree); - LocalVariableNode bindingNode = - (LocalVariableNode) ((binding == null) ? null : scan(binding, p)); - - instanceOfNode = new InstanceOfNode(tree, operand, bindingNode, refType, types); + Tree patternTree = InstanceOfUtils.getPattern(tree); + if (patternTree != null) { + Node pattern = scan(patternTree, p); + instanceOfNode = new InstanceOfNode(tree, operand, pattern, pattern.getType(), types); + } else { + TypeMirror refType = TreeUtils.typeOf(tree.getType()); + instanceOfNode = new InstanceOfNode(tree, operand, refType, types); + } extendWithNode(instanceOfNode); return instanceOfNode; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java index a424ef8b7dc..3fbbb50e034 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java @@ -384,4 +384,9 @@ public R visitMarker(MarkerNode n, P p) { public R visitExpressionStatement(ExpressionStatementNode n, P p) { return visitNode(n, p); } + + @Override + public R visitDeconstructorPattern(DeconstructorPatternNode n, P p) { + return visitNode(n, p); + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java index 80226d453c0..e3027fa44b9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java @@ -35,6 +35,9 @@ public class CaseNode extends Node { */ protected final List caseExprs; + /** The guard (the expression in the {@code when} clause) for this case. */ + protected final @Nullable Node guard; + /** * Create a new CaseNode. * @@ -42,14 +45,20 @@ public class CaseNode extends Node { * @param selectorExprAssignment the Node for the assignment of the switch selector expression to * a synthetic local variable * @param caseExprs the case expression(s) to match the switch expression against + * @param guard the guard expression or null * @param types a factory of utility methods for operating on types */ public CaseNode( - CaseTree tree, AssignmentNode selectorExprAssignment, List caseExprs, Types types) { + CaseTree tree, + AssignmentNode selectorExprAssignment, + List caseExprs, + @Nullable Node guard, + Types types) { super(types.getNoType(TypeKind.NONE)); this.tree = tree; this.selectorExprAssignment = selectorExprAssignment; this.caseExprs = caseExprs; + this.guard = guard; } /** @@ -72,6 +81,15 @@ public List getCaseOperands() { return caseExprs; } + /** + * Gets the node for the guard (the expression in the {@code when} clause). + * + * @return the node for the guard + */ + public @Nullable Node getGuard() { + return guard; + } + @Override public CaseTree getTree() { return tree; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java new file mode 100644 index 00000000000..840753dc35a --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java @@ -0,0 +1,94 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.Tree; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; + +/** A node for a deconstrutor pattern. */ +public class DeconstructorPatternNode extends Node { + /** + * The {@code DeconstructorPatternTree}, declared as {@link Tree} to permit this file to compile + * under JDK 20 and earlier. + */ + private final Tree deconstructorPattern; + + /** A list of nested pattern nodes. */ + private final List nestedPatterns; + + /** + * Creates a {@code DeconstructorPatternNode}. + * + * @param type the type of the node + * @param deconstructorPattern the {@code DeconstructorPatternTree} + * @param nestedPatterns a list of nested pattern nodes + */ + public DeconstructorPatternNode( + TypeMirror type, Tree deconstructorPattern, List nestedPatterns) { + super(type); + this.deconstructorPattern = deconstructorPattern; + this.nestedPatterns = nestedPatterns; + } + + @Override + @Pure + public @Nullable Tree getTree() { + return deconstructorPattern; + } + + /** + * Returns the nested patterns. + * + * @return the nested patterns + */ + @Pure + public List getNestedPatterns() { + return nestedPatterns; + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitDeconstructorPattern(this, p); + } + + @Override + @Pure + public Collection getOperands() { + return nestedPatterns; + } + + /** + * A list of nested binding variables. This is lazily initialized and should only be accessed by + * {@link #getBindingVariables()}. + */ + private @MonotonicNonNull List bindingVariables = null; + + /** + * Return all the binding variables in this pattern. + * + * @return all the binding variables in this pattern + */ + public List getBindingVariables() { + if (bindingVariables == null) { + if (nestedPatterns.isEmpty()) { + bindingVariables = Collections.emptyList(); + } else { + bindingVariables = new ArrayList<>(nestedPatterns.size()); + for (Node patternNode : nestedPatterns) { + if (patternNode instanceof LocalVariableNode) { + bindingVariables.add((LocalVariableNode) patternNode); + } else { + bindingVariables.addAll(((DeconstructorPatternNode) patternNode).getBindingVariables()); + } + } + bindingVariables = Collections.unmodifiableList(bindingVariables); + } + } + return bindingVariables; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java index bd8bc8c5051..5a17045323b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java @@ -3,10 +3,12 @@ import com.sun.source.tree.InstanceOfTree; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Objects; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.TypesUtils; @@ -27,8 +29,8 @@ public class InstanceOfNode extends Node { /** The tree associated with this node. */ protected final InstanceOfTree tree; - /** The node of the binding variable if one exists. */ - protected final @Nullable LocalVariableNode bindingVariable; + /** The node of the pattern if one exists. */ + protected final @Nullable Node patternNode; /** For Types.isSameType. */ protected final Types types; @@ -50,14 +52,14 @@ public InstanceOfNode(InstanceOfTree tree, Node operand, TypeMirror refType, Typ * * @param tree instanceof tree * @param operand the expression in the instanceof tree - * @param bindingVariable the binding variable or null if there is none + * @param patternNode the pattern node or null if there is none * @param refType the type in the instanceof * @param types types util */ public InstanceOfNode( InstanceOfTree tree, Node operand, - @Nullable LocalVariableNode bindingVariable, + @Nullable Node patternNode, TypeMirror refType, Types types) { super(types.getPrimitiveType(TypeKind.BOOLEAN)); @@ -65,7 +67,7 @@ public InstanceOfNode( this.operand = operand; this.refType = refType; this.types = types; - this.bindingVariable = bindingVariable; + this.patternNode = patternNode; } public Node getOperand() { @@ -76,9 +78,47 @@ public Node getOperand() { * Returns the binding variable for this instanceof, or null if one does not exist. * * @return the binding variable for this instanceof, or null if one does not exist + * @deprecated Use {@link #getPatternNode()} or {@link #getBindingVariables()} instead. */ + @Deprecated // 2023-09-24 public @Nullable LocalVariableNode getBindingVariable() { - return bindingVariable; + if (patternNode instanceof LocalVariableNode) { + return (LocalVariableNode) patternNode; + } + return null; + } + + /** + * A list of all binding variables in this instanceof. This is lazily initialized, use {@link + * #getBindingVariables()}. + */ + @MonotonicNonNull List bindingVariables = null; + + /** + * Return all the binding variables in this instanceof. + * + * @return all the binding variables in this instanceof + */ + public List getBindingVariables() { + if (bindingVariables == null) { + if (patternNode instanceof DeconstructorPatternNode) { + bindingVariables = ((DeconstructorPatternNode) patternNode).getBindingVariables(); + } else if (patternNode instanceof LocalVariableNode) { + bindingVariables = Collections.singletonList((LocalVariableNode) patternNode); + } else { + bindingVariables = Collections.emptyList(); + } + } + return bindingVariables; + } + + /** + * Returns the pattern for this instanceof, or null if one does not exist. + * + * @return the pattern for this instanceof, or null if one does not exist + */ + public @Nullable Node getPatternNode() { + return patternNode; } /** @@ -106,7 +146,7 @@ public String toString() { + getOperand() + " instanceof " + TypesUtils.simpleTypeName(getRefType()) - + (bindingVariable == null ? "" : " " + getBindingVariable()) + + (patternNode == null ? "" : " " + getPatternNode()) + ")"; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java index fb9cf62fe25..ad6be81daa9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java @@ -176,4 +176,13 @@ public interface NodeVisitor { * @return the return value of the operation implemented by this visitor */ R visitExpressionStatement(ExpressionStatementNode n, P p); + + /** + * Visits a deconstructor pattern node. + * + * @param n the {@link DeconstructorPatternNode} to be visited + * @param p the argument for the operation implemented by this visitor + * @return the return value of the operation implemented by this visitor + */ + R visitDeconstructorPattern(DeconstructorPatternNode n, P p); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6ec8df3d1e7..6fbc9aa9822 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,12 +3,18 @@ Version 3.39.0 (October 1, 2023) **User-visible changes:** -The Checker Framework runs under JDK 21 -- that is, it runs on a version 21 JVM. -It does not yet support all new Java 21 language features -- it may crash when -run on a program with new Java 21 language features. +The Checker Framework runs on a version 21 JVM. +It does not yet soundly check all new Java 21 language features, but it does not +crash when compiling them. **Implementation details:** +Dataflow supports all the new Java 21 langauge features. + * A new node,`DeconstructorPatternNode`, was added, so any implementation of + `NodeVisitor` must be updated. + * Method `InstanceOfNode.getBindingVariable()` is deprecated; use + `getPatternNode()` or `getBindingVariables()` instead. + WPI uses 1-based indexing for formal parameters and arguments. **Closed issues:** diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/CheckerFrameworkPerDirectoryTest.java b/framework-test/src/main/java/org/checkerframework/framework/test/CheckerFrameworkPerDirectoryTest.java index 3bbd347943e..4304eb64f0a 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/CheckerFrameworkPerDirectoryTest.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/CheckerFrameworkPerDirectoryTest.java @@ -141,8 +141,12 @@ protected CheckerFrameworkPerDirectoryTest( this.checkerOptions.add("-AajavaChecks"); } + /** Run the tests. */ @Test public void run() { + if (testFiles.isEmpty()) { + return; + } boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); List customizedOptions = customizeOptions(Collections.unmodifiableList(checkerOptions)); TestConfiguration config = diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java b/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java index b161ed9a7da..d8faaa9f4c6 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java @@ -58,6 +58,9 @@ public class TestUtilities { /** True if the JVM is version 18 or lower. */ public static final boolean IS_AT_MOST_18_JVM = SystemUtil.jreVersion <= 18; + /** True if the JVM is version 21 or above. */ + public static final boolean IS_AT_LEAST_21_JVM = SystemUtil.jreVersion >= 21; + static { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); OutputStream err = new ByteArrayOutputStream(); @@ -257,7 +260,8 @@ public static boolean isJavaTestFile(File file) { || (!IS_AT_LEAST_17_JVM && nextLine.contains("@below-java17-jdk-skip-test")) || (!IS_AT_MOST_17_JVM && nextLine.contains("@above-java17-jdk-skip-test")) || (!IS_AT_LEAST_18_JVM && nextLine.contains("@below-java18-jdk-skip-test")) - || (!IS_AT_MOST_18_JVM && nextLine.contains("@above-java18-jdk-skip-test"))) { + || (!IS_AT_MOST_18_JVM && nextLine.contains("@above-java18-jdk-skip-test")) + || (!IS_AT_LEAST_21_JVM && nextLine.contains("@below-java21-jdk-skip-test"))) { return false; } } diff --git a/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java b/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java index fc8f9c8497a..db1a965efb2 100644 --- a/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java @@ -224,17 +224,20 @@ public Void visitVariable(VariableTree tree, Void p) { VariableElement elt = TreeUtils.elementFromDeclaration(tree); if (elt.getKind().isField() && varType.hasExplicitAnnotation(Unique.class)) { checker.reportError(tree, "unique.location.forbidden"); - } else if (tree.getType().getKind() == Tree.Kind.ARRAY_TYPE) { - AnnotatedArrayType arrayType = (AnnotatedArrayType) varType; - if (arrayType.getComponentType().hasPrimaryAnnotation(Unique.class)) { - checker.reportError(tree, "unique.location.forbidden"); - } - } else if (tree.getType().getKind() == Tree.Kind.PARAMETERIZED_TYPE) { - AnnotatedDeclaredType declaredType = (AnnotatedDeclaredType) varType; - for (AnnotatedTypeMirror atm : declaredType.getTypeArguments()) { - if (atm.hasPrimaryAnnotation(Unique.class)) { + } else if (tree.getType() != null) { + // VariableTree#getType returns null for binding variables from a DeconstructionPatternTree. + if (tree.getType().getKind() == Tree.Kind.ARRAY_TYPE) { + AnnotatedArrayType arrayType = (AnnotatedArrayType) varType; + if (arrayType.getComponentType().hasPrimaryAnnotation(Unique.class)) { checker.reportError(tree, "unique.location.forbidden"); } + } else if (tree.getType().getKind() == Tree.Kind.PARAMETERIZED_TYPE) { + AnnotatedDeclaredType declaredType = (AnnotatedDeclaredType) varType; + for (AnnotatedTypeMirror atm : declaredType.getTypeArguments()) { + if (atm.hasPrimaryAnnotation(Unique.class)) { + checker.reportError(tree, "unique.location.forbidden"); + } + } } } return super.visitVariable(tree, p); diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 282d29047f4..6f900e062d1 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -141,9 +141,9 @@ import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TreeUtils.MemberReferenceKind; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.InstanceOfUtils; import org.checkerframework.javacutil.TypesUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.BindingPatternUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.InstanceOfUtils; import org.plumelib.util.ArrayMap; import org.plumelib.util.ArraySet; import org.plumelib.util.ArraysPlume; @@ -415,7 +415,10 @@ public Void scan(@Nullable Tree tree, Void p) { *

Subclasses may override this method to disable the test if even the option is provided. */ protected void testJointJavacJavaParserVisitor() { - if (root == null || !ajavaChecks) { + if (root == null + || !ajavaChecks + // TODO: Make annotation insertion work for Java 21. + || root.getSourceFile().toUri().toString().contains("java21")) { return; } @@ -461,7 +464,10 @@ public void defaultJointAction( *

Subclasses may override this method to disable the test even if the option is provided. */ protected void testAnnotationInsertion() { - if (root == null || !ajavaChecks) { + if (root == null + || !ajavaChecks + // TODO: Make annotation insertion work for Java 21. + || root.getSourceFile().toUri().toString().contains("java21")) { return; } @@ -1521,7 +1527,10 @@ protected void checkExplicitAnnotationsOnIntersectionBounds( public Void visitVariable(VariableTree tree, Void p) { warnAboutTypeAnnotationsTooEarly(tree, tree.getModifiers()); - visitAnnotatedType(tree.getModifiers().getAnnotations(), tree.getType()); + // VariableTree#getType returns null for binding variables from a DeconstructionPatternTree. + if (tree.getType() != null) { + visitAnnotatedType(tree.getModifiers().getAnnotations(), tree.getType()); + } AnnotatedTypeMirror variableType; if (getCurrentPath().getParentPath() != null @@ -2579,14 +2588,18 @@ public Void visitInstanceOf(InstanceOfTree tree, Void p) { // The "reference type" is the type after "instanceof". Tree patternTree = InstanceOfUtils.getPattern(tree); if (patternTree != null) { - VariableTree variableTree = BindingPatternUtils.getVariable(patternTree); - validateTypeOf(variableTree); - if (variableTree.getModifiers() != null) { - AnnotatedTypeMirror variableType = atypeFactory.getAnnotatedType(variableTree); - AnnotatedTypeMirror expType = atypeFactory.getAnnotatedType(tree.getExpression()); - if (!isTypeCastSafe(variableType, expType)) { - checker.reportWarning(tree, "instanceof.pattern.unsafe", expType, variableTree); + if (TreeUtils.isBindingPatternTree(patternTree)) { + VariableTree variableTree = BindingPatternUtils.getVariable(patternTree); + validateTypeOf(variableTree); + if (variableTree.getModifiers() != null) { + AnnotatedTypeMirror variableType = atypeFactory.getAnnotatedType(variableTree); + AnnotatedTypeMirror expType = atypeFactory.getAnnotatedType(tree.getExpression()); + if (!isTypeCastSafe(variableType, expType)) { + checker.reportWarning(tree, "instanceof.pattern.unsafe", expType, variableTree); + } } + } else { + // TODO: implement deconstructed patterns. } } else { Tree refTypeTree = tree.getType(); diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java index f7dfa2d2cf0..c4cd4ec3cca 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java @@ -26,8 +26,8 @@ import java.util.HashSet; import java.util.Set; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.BindingPatternUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; /** * After this visitor visits a tree, {@link #getTrees} returns all the trees that should match with diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java index 8e0299ad7fb..2882e95f8c1 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java @@ -161,11 +161,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.BindingPatternUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.CaseUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.InstanceOfUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.SwitchExpressionUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.YieldUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.InstanceOfUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.YieldUtils; /** * A visitor that processes javac trees and JavaParser nodes simultaneously, matching corresponding diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/TreeScannerWithDefaults.java b/framework/src/main/java/org/checkerframework/framework/ajava/TreeScannerWithDefaults.java index 8a72da509c7..77b1d09a629 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/TreeScannerWithDefaults.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/TreeScannerWithDefaults.java @@ -79,15 +79,16 @@ public abstract class TreeScannerWithDefaults extends TreeScanner { @Override public Void scan(Tree tree, Void unused) { if (tree != null && SystemUtil.jreVersion >= 14) { - if (tree.getKind().name().equals("SWITCH_EXPRESSION")) { - visitSwitchExpression17(tree, unused); - return null; - } else if (tree.getKind().name().equals("YIELD")) { - visitYield17(tree, unused); - return null; - } else if (tree.getKind().name().equals("BINDING_PATTERN")) { - visitBindingPattern17(tree, unused); - return null; + switch (tree.getKind().name()) { + case "SWITCH_EXPRESSION": + visitSwitchExpression17(tree, unused); + return null; + case "YIELD": + visitYield17(tree, unused); + return null; + case "BINDING_PATTERN": + visitBindingPattern17(tree, unused); + return null; } } return super.scan(tree, unused); diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java index e63ee1b0a67..dbd0555d667 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -971,9 +971,18 @@ public TransferResult visitMethodInvocation( @Override public TransferResult visitInstanceOf(InstanceOfNode node, TransferInput in) { TransferResult result = super.visitInstanceOf(node, in); + for (LocalVariableNode bindingVar : node.getBindingVariables()) { + JavaExpression expr = JavaExpression.fromNode(bindingVar); + AnnotatedTypeMirror expType = + analysis.atypeFactory.getAnnotatedType(node.getTree().getExpression()); + for (AnnotationMirror anno : expType.getPrimaryAnnotations()) { + in.getRegularStore().insertOrRefine(expr, anno); + } + } + // The "reference type" is the type after "instanceof". Tree refTypeTree = node.getTree().getType(); - if (refTypeTree.getKind() == Tree.Kind.ANNOTATED_TYPE) { + if (refTypeTree != null && refTypeTree.getKind() == Tree.Kind.ANNOTATED_TYPE) { AnnotatedTypeMirror refType = analysis.atypeFactory.getAnnotatedType(refTypeTree); AnnotatedTypeMirror expType = analysis.atypeFactory.getAnnotatedType(node.getTree().getExpression()); @@ -987,15 +996,6 @@ public TransferResult visitInstanceOf(InstanceOfNode node, TransferInput(result.getResultValue(), in.getRegularStore()); } } - // TODO: Should this be an else if? - if (node.getBindingVariable() != null) { - JavaExpression expr = JavaExpression.fromNode(node.getBindingVariable()); - AnnotatedTypeMirror expType = - analysis.atypeFactory.getAnnotatedType(node.getTree().getExpression()); - for (AnnotationMirror anno : expType.getPrimaryAnnotations()) { - in.getRegularStore().insertOrRefine(expr, anno); - } - } return result; } diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java index df7c830a47b..c6ed843ddb2 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromMemberVisitor.java @@ -43,6 +43,10 @@ public AnnotatedTypeMirror visitVariable(VariableTree variableTree, AnnotatedTyp result = f.getAnnotatedType(variableTree.getInitializer()); // Let normal defaulting happen for the primary annotation. result.clearPrimaryAnnotations(); + } else if (TreeUtils.isVariableTreeDeclaredUsingVar(variableTree) + || variableTree.getType() == null) { + // VariableTree#getType returns null for binding variables from a DeconstructionPatternTree. + result = f.type(variableTree); } else { // (variableTree.getType() does not include the annotation before the type, so those // are added to the type below). @@ -74,8 +78,12 @@ public AnnotatedTypeMirror visitVariable(VariableTree variableTree, AnnotatedTyp AnnotatedDeclaredType annotatedDeclaredType = (AnnotatedDeclaredType) result; // The underlying type of result does not have all annotations, but the TypeMirror of // variableTree.getType() does. - DeclaredType declaredType = (DeclaredType) TreeUtils.typeOf(variableTree.getType()); - AnnotatedTypes.applyAnnotationsFromDeclaredType(annotatedDeclaredType, declaredType); + // VariableTree#getType returns null for binding variables from a DeconstructionPatternTree. + if (variableTree.getType() != null + && !TreeUtils.isVariableTreeDeclaredUsingVar(variableTree)) { + DeclaredType declaredType = (DeclaredType) TreeUtils.typeOf(variableTree.getType()); + AnnotatedTypes.applyAnnotationsFromDeclaredType(annotatedDeclaredType, declaredType); + } // Handle declaration annotations for (AnnotationMirror anno : modifierAnnos) { diff --git a/framework/tests/all-systems/java21/Issue6173.java b/framework/tests/all-systems/java21/Issue6173.java new file mode 100644 index 00000000000..bf48e909f18 --- /dev/null +++ b/framework/tests/all-systems/java21/Issue6173.java @@ -0,0 +1,26 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +public class Issue6173 { + + static Object toGroupByQueryWithExtractor2(GroupBy groupBy) { + return switch (groupBy) { + case OWNER -> new Object(); + case CHANNEL -> new Object(); + case TOPIC -> new Object(); + case TEAM -> new Object(); + case null -> throw new IllegalArgumentException("GroupBy parameter is required"); + }; + } + + public enum GroupBy { + OWNER, + CHANNEL, + TOPIC, + TEAM; + } +} diff --git a/framework/tests/all-systems/java21/JEP440.java b/framework/tests/all-systems/java21/JEP440.java new file mode 100644 index 00000000000..6af52a81c04 --- /dev/null +++ b/framework/tests/all-systems/java21/JEP440.java @@ -0,0 +1,76 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +// JEP 440: Record Patterns +// These are examples copied from: +// https://openjdk.org/jeps/440 + +@SuppressWarnings("i18n") // true postives. +public class JEP440 { + + record Point(int x, int y) {} + + static void printSum(Object obj) { + if (obj instanceof Point(int x, int y)) { + System.out.println(x + y); + } + } + + enum Color {RED, GREEN, BLUE} + + record ColoredPoint(Point p, Color c) {} + + record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {} + + static void printUpperLeftColoredPoint(Rectangle r) { + if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) { + System.out.println(ul.c()); + } + } + + static void printColorOfUpperLeftPoint(Rectangle r) { + if (r instanceof Rectangle( + ColoredPoint(Point p, Color c), + ColoredPoint lr + )) { + System.out.println(c); + } + } + + static 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); + } + } + + void failToMatch() { + record Pair(Object x, Object y) {} + Pair p = new Pair(42, 42); + if (p instanceof Pair(String s, String t)) { + System.out.println(s + ", " + t); + } else { + System.out.println("Not a pair of strings"); + } + } + + record Box(T t) {} + + static void test1(Box> bbs) { + if (bbs instanceof Box>(Box(var s))) { + System.out.println("String " + s); + } + } + + static void test2(Box> bbs) { + if (bbs instanceof Box(Box(var s))) { + System.out.println("String " + s); + } + } +} diff --git a/framework/tests/all-systems/java21/JEP441.java b/framework/tests/all-systems/java21/JEP441.java new file mode 100644 index 00000000000..051c7142728 --- /dev/null +++ b/framework/tests/all-systems/java21/JEP441.java @@ -0,0 +1,230 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +// These are examples copied from: +// https://openjdk.org/jeps/441 + +@SuppressWarnings("i18n") // true postives. +public class JEP441 { + + // JEP 441 enhances switch statements and expressions in four ways: + // * Improve enum constant case labels + // * Extend case labels to include patterns and null in addition to constants + // * Broaden the range of types permitted for the selector expressions of both switch statements + // and switch expressions (along with the required richer analysis of exhaustiveness of switch + // blocks) + // * Allow optional when clauses to follow case labels. + + // Prior to Java 21 + static String formatter(Object obj) { + String formatted = "unknown"; + if (obj instanceof Integer i) { + formatted = String.format("int %d", i); + } else if (obj instanceof Long l) { + formatted = String.format("long %d", l); + } else if (obj instanceof Double d) { + formatted = String.format("double %f", d); + } else if (obj instanceof String s) { + formatted = String.format("String %s", s); + } + return formatted; + } + static void formatterPatternSwitchStatement(Object obj) { + switch (obj) { + case Integer i: String.format("int %d", i); break; + case Long l : String.format("long %d", l); break; + case Double d : String.format("double %f", d); break; + case String s : String.format("String %s", s); break; + default : obj.toString(); + }; + } + + static String formatterPatternSwitch(Object obj) { + return switch (obj) { + case Integer i -> String.format("int %d", i); + case Long l -> String.format("long %d", l); + case Double d -> String.format("double %f", d); + case String s -> String.format("String %s", s); + default -> obj.toString(); + }; + } + // As of Java 21 + static void testFooBarNew(String s) { + switch (s) { + case null -> System.out.println("Oops"); + case "Foo", "Bar" -> System.out.println("Great"); + default -> System.out.println("Ok"); + } + } + + static void testStringOld(String response) { + switch (response) { + case null -> { } + case String s -> { + if (s.equalsIgnoreCase("YES")) + System.out.println("You got it"); + else if (s.equalsIgnoreCase("NO")) + System.out.println("Shame"); + else + System.out.println("Sorry?"); + } + } + } + + static void testStringNew(String response) { + switch (response) { + case null -> { } + case String s + when s.equalsIgnoreCase("YES") -> { + System.out.println("You got it"); + } + case String s + when s.equalsIgnoreCase("NO") -> { + System.out.println("Shame"); + } + case String s -> { + System.out.println("Sorry?"); + } + } + } + + static void testStringEnhanced(String response) { + switch (response) { + case null -> { } + case "y", "Y" -> { + System.out.println("You got it"); + } + case "n", "N" -> { + System.out.println("Shame"); + } + case String s + when s.equalsIgnoreCase("YES") -> { + System.out.println("You got it"); + } + case String s + when s.equalsIgnoreCase("NO") -> { + System.out.println("Shame"); + } + case String s -> { + System.out.println("Sorry?"); + } + } + } + + sealed interface CardClassification permits Suit, Tarot {} + public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES } + final class Tarot implements CardClassification {} + + static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) { + switch (c) { + case Suit s when s == Suit.CLUBS -> { + System.out.println("It's clubs"); + } + case Suit s when s == Suit.DIAMONDS -> { + System.out.println("It's diamonds"); + } + case Suit s when s == Suit.HEARTS -> { + System.out.println("It's hearts"); + } + case Suit s -> { + System.out.println("It's spades"); + } + case Tarot t -> { + System.out.println("It's a tarot"); + } + } + } + + static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) { + switch (c) { + case Suit.CLUBS -> { + System.out.println("It's clubs"); + } + case Suit.DIAMONDS -> { + System.out.println("It's diamonds"); + } + case Suit.HEARTS -> { + System.out.println("It's hearts"); + } + case Suit.SPADES -> { + System.out.println("It's spades"); + } + case Tarot t -> { + System.out.println("It's a tarot"); + } + } + } + sealed interface Currency permits Coin {} + enum Coin implements Currency { HEADS, TAILS } + + static void goodEnumSwitch1(Currency c) { + switch (c) { + case Coin.HEADS -> { // Qualified name of enum constant as a label + System.out.println("Heads"); + } + case Coin.TAILS -> { + System.out.println("Tails"); + } + } + } + + static void goodEnumSwitch2(Coin c) { + switch (c) { + case HEADS -> { + System.out.println("Heads"); + } + case Coin.TAILS -> { // Unnecessary qualification but allowed + System.out.println("Tails"); + } + } + } + + record Point(int i, int j) {} + enum Color { RED, GREEN, BLUE; } + + static void typeTester(Object obj) { + switch (obj) { + case null -> System.out.println("null"); + case String s -> System.out.println("String"); + case Color c -> System.out.println("Color: " + c.toString()); + case Point p -> System.out.println("Record class: " + p.toString()); + case int[] ia -> System.out.println("Array of ints of length" + ia.length); + default -> System.out.println("Something else"); + } + } + + static void first(Object obj) { + switch (obj) { + case String s -> + System.out.println("A string: " + s); + case CharSequence cs -> + System.out.println("A sequence of length " + cs.length()); + default -> { + break; + } + } + } + + record MyPair(S fst, T snd){}; + + static void recordInference(MyPair pair) { + switch (pair) { + case MyPair(var f, var s) -> { + String ff = f; + Integer ss = s; + } + } + } + void fragment( Integer i ){ + // TODO: This would be a good test case for the Value Checker. +// switch (i) { +// case -1, 1 -> ... // Special cases +// case Integer j when j > 0 -> ... // Positive integer cases +// case Integer j -> ... // All the remaining integers +// } + } +} diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/SwitchExpressionScanner.java b/javacutil/src/main/java/org/checkerframework/javacutil/SwitchExpressionScanner.java index eb82e041898..1ae0cd41f57 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/SwitchExpressionScanner.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/SwitchExpressionScanner.java @@ -9,9 +9,9 @@ import java.util.function.BiFunction; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.CaseUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.SwitchExpressionUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.YieldUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.YieldUtils; /** * A class that visits each result expression of a switch expression and calls {@link diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index ce026017eea..d0f3cfbe6cb 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -88,11 +88,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.dataflow.qual.Pure; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.BindingPatternUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.CaseUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.InstanceOfUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.SwitchExpressionUtils; -import org.checkerframework.javacutil.trees.TreeUtilsAfterJava11.YieldUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.InstanceOfUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.YieldUtils; import org.plumelib.util.CollectionsPlume; import org.plumelib.util.UniqueIdMap; @@ -1668,7 +1668,7 @@ public static boolean isTypeDeclaration(Tree tree) { } /** - * Returns whether or not tree is an access of array length. + * Returns true if tree is an access of array length. * * @param tree tree to check * @return true if tree is an access of array length @@ -2220,30 +2220,31 @@ public static boolean sameTree(ExpressionTree expr1, ExpressionTree expr2) { } /** - * Returns true if this is the default case for a switch statement or expression. + * Returns true if this is the default case for a switch statement or expression. (Also, returns + * true if {@code caseTree} is {@code case null, default:}.) * * @param caseTree a case tree * @return true if {@code caseTree} is the default case for a switch statement or expression */ public static boolean isDefaultCaseTree(CaseTree caseTree) { - return CaseUtils.getLabels(caseTree).isEmpty(); + return CaseUtils.isDefaultCaseTree(caseTree); } /** - * Returns whether {@code tree} is a {@code DefaultCaseLabelTree}. + * Returns true if {@code tree} is a {@code DefaultCaseLabelTree}. * * @param tree a tree to check - * @return whether {@code tree} is a {@code DefaultCaseLabelTree} + * @return true if {@code tree} is a {@code DefaultCaseLabelTree} */ public static boolean isDefaultCaseLabelTree(Tree tree) { return tree.getKind().name().contentEquals("DEFAULT_CASE_LABEL"); } /** - * Returns whether {@code tree} is a {@code ConstantCaseLabelTree}. + * Returns true if {@code tree} is a {@code ConstantCaseLabelTree}. * * @param tree a tree to check - * @return whether {@code tree} is a {@code ConstantCaseLabelTree} + * @return true if {@code tree} is a {@code ConstantCaseLabelTree} */ public static boolean isConstantCaseLabelTree(Tree tree) { return tree.getKind().name().contentEquals("CONSTANT_CASE_LABEL"); @@ -2253,7 +2254,7 @@ public static boolean isConstantCaseLabelTree(Tree tree) { * Returns whether {@code tree} is a {@code PatternCaseLabelTree}. * * @param tree a tree to check - * @return whether {@code tree} is a {@code PatternCaseLabelTree} + * @return true if {@code tree} is a {@code PatternCaseLabelTree} */ public static boolean isPatternCaseLabelTree(Tree tree) { return tree.getKind().name().contentEquals("PATTERN_CASE_LABEL"); @@ -2299,6 +2300,16 @@ public static List caseTreeGetExpressions(CaseTree cas return CaseUtils.getBody(caseTree); } + /** + * Returns true if {@code tree} is a {@code BindingPatternTree}. + * + * @param tree a tree to check + * @return true if {@code tree} is a {@code BindingPatternTree} + */ + public static boolean isBindingPatternTree(Tree tree) { + return tree.getKind().name().contentEquals("BINDING_PATTERN"); + } + /** * Returns the binding variable of {@code bindingPatternTree}. * @@ -2311,6 +2322,16 @@ public static VariableTree bindingPatternTreeGetVariable(Tree bindingPatternTree return BindingPatternUtils.getVariable(bindingPatternTree); } + /** + * Returns true if {@code tree} is a {@code DeconstructionPatternTree}. + * + * @param tree a tree to check + * @return true if {@code tree} is a {@code DeconstructionPatternTree} + */ + public static boolean isDeconstructionPatternTree(Tree tree) { + return tree.getKind().name().contentEquals("DECONSTRUCTION_PATTERN"); + } + /** * Returns the pattern of {@code instanceOfTree} tree. Returns null if the instanceof does not * have a pattern, including if the JDK version does not support instance-of patterns. diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeUtilsAfterJava11.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava11.java similarity index 86% rename from javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeUtilsAfterJava11.java rename to javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava11.java index a5f1d7eeaf1..b780e617d47 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeUtilsAfterJava11.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtilsAfterJava11.java @@ -1,4 +1,4 @@ -package org.checkerframework.javacutil.trees; +package org.checkerframework.javacutil; import com.sun.source.tree.CaseTree; import com.sun.source.tree.ExpressionTree; @@ -13,8 +13,6 @@ import javax.lang.model.SourceVersion; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; -import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.TreeUtils; /** * This class contains util methods for reflective accessing Tree classes and methods that were @@ -116,14 +114,62 @@ public static boolean isCaseRule(CaseTree caseTree) { } /** - * Get the list of labels from a case expression. For {@code default}, this is empty. Otherwise, - * in JDK 11 and earlier, this is a singleton list of expression trees. In JDK 12, this is a - * list of expression trees. In JDK 21+, this is a list of expression and pattern trees. + * Returns true if this is the default case for a switch statement or expression. (Also, returns + * true if {@code caseTree} is {@code case null, default:}.) + * + * @param caseTree a case tree + * @return true if {@code caseTree} is the default case for a switch statement or expression + */ + public static boolean isDefaultCaseTree(CaseTree caseTree) { + if (sourceVersionNumber >= 21) { + for (Tree label : getLabels(caseTree, true)) { + if (TreeUtils.isDefaultCaseLabelTree(label)) { + return true; + } + } + return false; + } else { + return getExpressions(caseTree).isEmpty(); + } + } + + /** + * Get the list of labels from a case expression. For {@code default}, this is empty. For {@code + * case null, default}, the list contains {@code null}. Otherwise, in JDK 11 and earlier, this + * is a list of a single expression tree. In JDK 12+, the list may have multiple expression + * trees. In JDK 21+, the list might contain a single pattern tree. * * @param caseTree the case expression to get the labels from * @return the list of case labels in the case */ public static List getLabels(CaseTree caseTree) { + return getLabels(caseTree, false); + } + + /** + * Get the list of labels from a case expression. + * + *

For JDKs before 21, if {@code caseTree} is the default case, then the returned list is + * empty. + * + *

For 21+ JDK, if {@code useDefaultCaseLabelTree} is false, then if {@code caseTree} is the + * default case or {@code case null, default}, then the returned list is empty. If {@code + * useDefaultCaseLabelTree} is true, then if {@code caseTree} is the default case the returned + * contains just a {@code DefaultCaseLabelTree}. If {@code useDefaultCaseLabelTree} is false, + * then if {@code caseTree} is {@code case null, default} the returned list is a {@code + * DefaultCaseLabelTree} and the expression tree for {@code null}. + * + *

Otherwise, in JDK 11 and earlier, this is a list of a single expression tree. In JDK 12+, + * the list may have multiple expression trees. In JDK 21+, the list might contain a single + * pattern tree. + * + * @param caseTree the case expression to get the labels from + * @param useDefaultCaseLabelTree weather the result should contain a {@code + * DefaultCaseLabelTree}. + * @return the list of case labels in the case + */ + private static List getLabels( + CaseTree caseTree, boolean useDefaultCaseLabelTree) { if (sourceVersionNumber >= 21) { if (GET_LABELS == null) { GET_LABELS = getMethod(CaseTree.class, "getLabels"); @@ -131,17 +177,19 @@ public static List getLabels(CaseTree caseTree) { @SuppressWarnings("unchecked") List caseLabelTrees = (List) invokeNonNullResult(GET_LABELS, caseTree); - List unWrappedLabels = new ArrayList<>(); + List labels = new ArrayList<>(); for (Tree caseLabel : caseLabelTrees) { if (TreeUtils.isDefaultCaseLabelTree(caseLabel)) { - return Collections.emptyList(); + if (useDefaultCaseLabelTree) { + labels.add(caseLabel); + } } else if (TreeUtils.isConstantCaseLabelTree(caseLabel)) { - unWrappedLabels.add(ConstantCaseLabelUtils.getConstantExpression(caseLabel)); + labels.add(ConstantCaseLabelUtils.getConstantExpression(caseLabel)); } else if (TreeUtils.isPatternCaseLabelTree(caseLabel)) { - unWrappedLabels.add(PatternCaseLabelUtils.getPattern(caseLabel)); + labels.add(PatternCaseLabelUtils.getPattern(caseLabel)); } } - return unWrappedLabels; + return labels; } return getExpressions(caseTree); } @@ -184,7 +232,7 @@ public static List getExpressions(CaseTree caseTree) { if (GET_GUARD == null) { GET_GUARD = getMethod(CaseTree.class, "getGuard"); } - return (ExpressionTree) invokeNonNullResult(GET_GUARD, caseTree); + return (ExpressionTree) invoke(GET_GUARD, caseTree); } }