8268871: Adjust javac to updated exhaustiveness specification
Reviewed-by: vromero
This commit is contained in:
parent
44691cc3b0
commit
4eb321298a
@ -30,7 +30,7 @@ package com.sun.tools.javac.comp;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.sun.source.tree.LambdaExpressionTree.BodyKind;
|
||||
import com.sun.tools.javac.code.*;
|
||||
@ -45,17 +45,16 @@ import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
|
||||
import com.sun.tools.javac.util.JCDiagnostic.Error;
|
||||
import com.sun.tools.javac.util.JCDiagnostic.Warning;
|
||||
|
||||
import com.sun.tools.javac.code.Kinds.Kind;
|
||||
import com.sun.tools.javac.code.Symbol.*;
|
||||
import com.sun.tools.javac.tree.JCTree.*;
|
||||
|
||||
import static com.sun.tools.javac.code.Flags.*;
|
||||
import static com.sun.tools.javac.code.Flags.BLOCK;
|
||||
import static com.sun.tools.javac.code.Kinds.Kind.*;
|
||||
import com.sun.tools.javac.code.Type.TypeVar;
|
||||
import static com.sun.tools.javac.code.TypeTag.BOOLEAN;
|
||||
import static com.sun.tools.javac.code.TypeTag.VOID;
|
||||
import com.sun.tools.javac.resources.CompilerProperties.Fragments;
|
||||
import com.sun.tools.javac.tree.JCTree.JCParenthesizedPattern;
|
||||
import static com.sun.tools.javac.tree.JCTree.Tag.*;
|
||||
import com.sun.tools.javac.util.JCDiagnostic.Fragment;
|
||||
|
||||
@ -665,7 +664,7 @@ public class Flow {
|
||||
ListBuffer<PendingExit> prevPendingExits = pendingExits;
|
||||
pendingExits = new ListBuffer<>();
|
||||
scan(tree.selector);
|
||||
Set<Object> constants = tree.patternSwitch ? allSwitchConstants(tree.selector) : null;
|
||||
Set<Symbol> constants = tree.patternSwitch ? new HashSet<>() : null;
|
||||
for (List<JCCase> l = tree.cases; l.nonEmpty(); l = l.tail) {
|
||||
alive = Liveness.ALIVE;
|
||||
JCCase c = l.head;
|
||||
@ -687,8 +686,9 @@ public class Flow {
|
||||
l.tail.head.pos(),
|
||||
Warnings.PossibleFallThroughIntoCase);
|
||||
}
|
||||
if ((constants == null || !constants.isEmpty()) && !tree.hasTotalPattern &&
|
||||
tree.patternSwitch && !TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) {
|
||||
if (!tree.hasTotalPattern && tree.patternSwitch &&
|
||||
!TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases) &&
|
||||
(constants == null || !isExhaustive(tree.selector.type, constants))) {
|
||||
log.error(tree, Errors.NotExhaustiveStatement);
|
||||
}
|
||||
if (!tree.hasTotalPattern) {
|
||||
@ -702,7 +702,7 @@ public class Flow {
|
||||
ListBuffer<PendingExit> prevPendingExits = pendingExits;
|
||||
pendingExits = new ListBuffer<>();
|
||||
scan(tree.selector);
|
||||
Set<Object> constants = allSwitchConstants(tree.selector);
|
||||
Set<Symbol> constants = new HashSet<>();
|
||||
Liveness prevAlive = alive;
|
||||
for (List<JCCase> l = tree.cases; l.nonEmpty(); l = l.tail) {
|
||||
alive = Liveness.ALIVE;
|
||||
@ -723,47 +723,83 @@ public class Flow {
|
||||
}
|
||||
c.completesNormally = alive != Liveness.DEAD;
|
||||
}
|
||||
if ((constants == null || !constants.isEmpty()) && !tree.hasTotalPattern &&
|
||||
!TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) {
|
||||
if (!tree.hasTotalPattern && !TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases) &&
|
||||
!isExhaustive(tree.selector.type, constants)) {
|
||||
log.error(tree, Errors.NotExhaustive);
|
||||
}
|
||||
alive = prevAlive;
|
||||
alive = alive.or(resolveYields(tree, prevPendingExits));
|
||||
}
|
||||
|
||||
private Set<Object> allSwitchConstants(JCExpression selector) {
|
||||
Set<Object> constants = null;
|
||||
TypeSymbol selectorSym = selector.type.tsym;
|
||||
if ((selectorSym.flags() & ENUM) != 0) {
|
||||
constants = new HashSet<>();
|
||||
Predicate<Symbol> enumConstantFilter =
|
||||
s -> (s.flags() & ENUM) != 0 && s.kind == Kind.VAR;
|
||||
for (Symbol s : selectorSym.members().getSymbols(enumConstantFilter)) {
|
||||
constants.add(s.name);
|
||||
}
|
||||
} else if (selectorSym.isAbstract() && selectorSym.isSealed() && selectorSym.kind == Kind.TYP) {
|
||||
constants = new HashSet<>();
|
||||
constants.addAll(((ClassSymbol) selectorSym).permitted);
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
|
||||
private void handleConstantCaseLabel(Set<Object> constants, JCCaseLabel pat) {
|
||||
private void handleConstantCaseLabel(Set<Symbol> constants, JCCaseLabel pat) {
|
||||
if (constants != null) {
|
||||
if (pat.isExpression()) {
|
||||
JCExpression expr = (JCExpression) pat;
|
||||
if (expr.hasTag(IDENT))
|
||||
constants.remove(((JCIdent) expr).name);
|
||||
if (expr.hasTag(IDENT) && ((JCIdent) expr).sym.isEnum())
|
||||
constants.add(((JCIdent) expr).sym);
|
||||
} else if (pat.isPattern()) {
|
||||
PatternPrimaryType patternType = TreeInfo.primaryPatternType((JCPattern) pat);
|
||||
|
||||
if (patternType.unconditional()) {
|
||||
constants.remove(patternType.type().tsym);
|
||||
constants.add(patternType.type().tsym);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void transitiveCovers(Set<Symbol> covered) {
|
||||
List<Symbol> todo = List.from(covered);
|
||||
while (todo.nonEmpty()) {
|
||||
Symbol sym = todo.head;
|
||||
todo = todo.tail;
|
||||
switch (sym.kind) {
|
||||
case VAR -> {
|
||||
Iterable<Symbol> constants = sym.owner
|
||||
.members()
|
||||
.getSymbols(s -> s.isEnum() &&
|
||||
s.kind == VAR);
|
||||
boolean hasAll = StreamSupport.stream(constants.spliterator(), false)
|
||||
.allMatch(covered::contains);
|
||||
|
||||
if (hasAll && covered.add(sym.owner)) {
|
||||
todo = todo.prepend(sym.owner);
|
||||
}
|
||||
}
|
||||
|
||||
case TYP -> {
|
||||
for (Type sup : types.directSupertypes(sym.type)) {
|
||||
if (sup.tsym.kind == TYP && sup.tsym.isAbstract() && sup.tsym.isSealed()) {
|
||||
boolean hasAll = ((ClassSymbol) sup.tsym).permitted
|
||||
.stream()
|
||||
.allMatch(covered::contains);
|
||||
|
||||
if (hasAll && covered.add(sup.tsym)) {
|
||||
todo = todo.prepend(sup.tsym);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExhaustive(Type seltype, Set<Symbol> covered) {
|
||||
transitiveCovers(covered);
|
||||
return switch (seltype.getTag()) {
|
||||
case CLASS -> {
|
||||
if (seltype.isCompound()) {
|
||||
if (seltype.isIntersection()) {
|
||||
yield ((Type.IntersectionClassType) seltype).getComponents().stream().anyMatch(t -> isExhaustive(t, covered));
|
||||
}
|
||||
yield false;
|
||||
}
|
||||
yield covered.contains(seltype.tsym);
|
||||
}
|
||||
case TYPEVAR -> isExhaustive(((TypeVar) seltype).getUpperBound(), covered);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public void visitTry(JCTry tree) {
|
||||
ListBuffer<PendingExit> prevPendingExits = pendingExits;
|
||||
pendingExits = new ListBuffer<>();
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @bug 8262891
|
||||
* @bug 8262891 8268871
|
||||
* @summary Check exhaustiveness of switches over sealed types.
|
||||
* @library /tools/lib
|
||||
* @modules jdk.compiler/com.sun.tools.javac.api
|
||||
@ -369,49 +369,6 @@ public class Exhaustiveness extends TestRunner {
|
||||
""");
|
||||
}
|
||||
|
||||
private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException {
|
||||
Path current = base.resolve(".");
|
||||
Path libSrc = current.resolve("lib-src");
|
||||
for (String code : libraryCode) {
|
||||
tb.writeJavaFiles(libSrc, code);
|
||||
}
|
||||
|
||||
Path libClasses = current.resolve("libClasses");
|
||||
|
||||
Files.createDirectories(libClasses);
|
||||
|
||||
new JavacTask(tb)
|
||||
.options("--enable-preview",
|
||||
"-source", JAVA_VERSION)
|
||||
.outdir(libClasses)
|
||||
.files(tb.findJavaFiles(libSrc))
|
||||
.run();
|
||||
|
||||
Path src = current.resolve("src");
|
||||
tb.writeJavaFiles(src, testCode);
|
||||
|
||||
Path classes = current.resolve("libClasses");
|
||||
|
||||
Files.createDirectories(libClasses);
|
||||
|
||||
var log =
|
||||
new JavacTask(tb)
|
||||
.options("--enable-preview",
|
||||
"-source", JAVA_VERSION,
|
||||
"-XDrawDiagnostics",
|
||||
"-Xlint:-preview",
|
||||
"--class-path", libClasses.toString())
|
||||
.outdir(classes)
|
||||
.files(tb.findJavaFiles(src))
|
||||
.run(expectedErrors.length > 0 ? Task.Expect.FAIL : Task.Expect.SUCCESS)
|
||||
.writeAll()
|
||||
.getOutputLines(Task.OutputKind.DIRECT);
|
||||
if (expectedErrors.length > 0 && !List.of(expectedErrors).equals(log)) {
|
||||
throw new AssertionError("Incorrect errors, expected: " + List.of(expectedErrors) +
|
||||
", actual: " + log);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInaccessiblePermitted(Path base) throws IOException {
|
||||
Path current = base.resolve(".");
|
||||
@ -640,4 +597,225 @@ public class Exhaustiveness extends TestRunner {
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExhaustiveTransitive(Path base) throws Exception {
|
||||
doTest(base,
|
||||
new String[]{"""
|
||||
package lib;
|
||||
public sealed interface S permits A, B {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class A implements S {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public abstract sealed class B implements S permits C, D {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class C extends B {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class D extends B {}
|
||||
"""},
|
||||
"""
|
||||
package test;
|
||||
import lib.*;
|
||||
public class Test {
|
||||
private int test(S obj, boolean b) {
|
||||
return switch (obj) {
|
||||
case A a -> 0;
|
||||
case C c && b -> 0;
|
||||
case C c -> 0;
|
||||
case D d -> 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotExhaustiveTransitive(Path base) throws Exception {
|
||||
doTest(base,
|
||||
new String[]{"""
|
||||
package lib;
|
||||
public sealed interface S permits A, B {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class A implements S {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public abstract sealed class B implements S permits C, D {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class C extends B {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class D extends B {}
|
||||
"""},
|
||||
"""
|
||||
package test;
|
||||
import lib.*;
|
||||
public class Test {
|
||||
private int test(S obj, boolean b) {
|
||||
return switch (obj) {
|
||||
case A a -> 0;
|
||||
case C c -> 0;
|
||||
case D d && b -> 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
""",
|
||||
"Test.java:5:16: compiler.err.not.exhaustive",
|
||||
"- compiler.note.preview.filename: Test.java, DEFAULT",
|
||||
"- compiler.note.preview.recompile",
|
||||
"1 error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExhaustiveIntersection(Path base) throws Exception {
|
||||
doTest(base,
|
||||
new String[]{"""
|
||||
package lib;
|
||||
public sealed interface S permits A, B {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public abstract class Base {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public interface Marker {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class A extends Base implements S, Marker {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public abstract sealed class B extends Base implements S permits C, D {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class C extends B implements Marker {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class D extends B implements Marker {}
|
||||
"""},
|
||||
"""
|
||||
package test;
|
||||
import lib.*;
|
||||
public class Test {
|
||||
private <T extends Base & S & Marker> int test(T obj, boolean b) {
|
||||
return switch (obj) {
|
||||
case A a -> 0;
|
||||
case C c && b -> 0;
|
||||
case C c -> 0;
|
||||
case D d -> 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotExhaustiveIntersection(Path base) throws Exception {
|
||||
doTest(base,
|
||||
new String[]{"""
|
||||
package lib;
|
||||
public sealed interface S permits A, B {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public abstract class Base {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public interface Marker {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class A extends Base implements S, Marker {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public abstract sealed class B extends Base implements S permits C, D {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class C extends B implements Marker {}
|
||||
""",
|
||||
"""
|
||||
package lib;
|
||||
public final class D extends B implements Marker {}
|
||||
"""},
|
||||
"""
|
||||
package test;
|
||||
import lib.*;
|
||||
public class Test {
|
||||
private <T extends Base & S & Marker> int test(T obj, boolean b) {
|
||||
return switch (obj) {
|
||||
case A a -> 0;
|
||||
case C c -> 0;
|
||||
case D d && b -> 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
""",
|
||||
"Test.java:5:16: compiler.err.not.exhaustive",
|
||||
"- compiler.note.preview.filename: Test.java, DEFAULT",
|
||||
"- compiler.note.preview.recompile",
|
||||
"1 error");
|
||||
}
|
||||
|
||||
private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException {
|
||||
Path current = base.resolve(".");
|
||||
Path libSrc = current.resolve("lib-src");
|
||||
for (String code : libraryCode) {
|
||||
tb.writeJavaFiles(libSrc, code);
|
||||
}
|
||||
|
||||
Path libClasses = current.resolve("libClasses");
|
||||
|
||||
Files.createDirectories(libClasses);
|
||||
|
||||
new JavacTask(tb)
|
||||
.options("--enable-preview",
|
||||
"-source", JAVA_VERSION)
|
||||
.outdir(libClasses)
|
||||
.files(tb.findJavaFiles(libSrc))
|
||||
.run();
|
||||
|
||||
Path src = current.resolve("src");
|
||||
tb.writeJavaFiles(src, testCode);
|
||||
|
||||
Path classes = current.resolve("libClasses");
|
||||
|
||||
Files.createDirectories(libClasses);
|
||||
|
||||
var log =
|
||||
new JavacTask(tb)
|
||||
.options("--enable-preview",
|
||||
"-source", JAVA_VERSION,
|
||||
"-XDrawDiagnostics",
|
||||
"-Xlint:-preview",
|
||||
"--class-path", libClasses.toString())
|
||||
.outdir(classes)
|
||||
.files(tb.findJavaFiles(src))
|
||||
.run(expectedErrors.length > 0 ? Task.Expect.FAIL : Task.Expect.SUCCESS)
|
||||
.writeAll()
|
||||
.getOutputLines(Task.OutputKind.DIRECT);
|
||||
if (expectedErrors.length > 0 && !List.of(expectedErrors).equals(log)) {
|
||||
throw new AssertionError("Incorrect errors, expected: " + List.of(expectedErrors) +
|
||||
", actual: " + log);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user