8268871: Adjust javac to updated exhaustiveness specification

Reviewed-by: vromero
This commit is contained in:
Jan Lahoda 2021-06-25 09:52:06 +00:00
parent 44691cc3b0
commit 4eb321298a
2 changed files with 288 additions and 74 deletions

View File

@ -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<>();

View File

@ -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);
}
}
}