8364991: Incorrect not-exhaustive error

Reviewed-by: vromero
This commit is contained in:
Jan Lahoda
2025-11-18 13:55:42 +00:00
parent 36b66e13c8
commit 2e68b79a39
2 changed files with 285 additions and 41 deletions

View File

@@ -39,6 +39,7 @@ import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.code.Kinds.Kind;
import com.sun.tools.javac.code.Type.TypeVar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Predicate;
@@ -60,6 +61,7 @@ public class ExhaustivenessComputer {
private final Types types;
private final Check chk;
private final Infer infer;
private final Map<Pair<Type, Type>, Boolean> isSubtypeCache = new HashMap<>();
public static ExhaustivenessComputer instance(Context context) {
ExhaustivenessComputer instance = context.get(exhaustivenessKey);
@@ -120,6 +122,7 @@ public class ExhaustivenessComputer {
}
}
Set<PatternDescription> patterns = patternSet;
Set<Set<PatternDescription>> seenFallback = new HashSet<>();
boolean useHashes = true;
try {
boolean repeat = true;
@@ -141,7 +144,7 @@ public class ExhaustivenessComputer {
//but hashing in reduceNestedPatterns will not allow that
//disable the use of hashing, and use subtyping in
//reduceNestedPatterns to handle situations like this:
repeat = useHashes;
repeat = useHashes && seenFallback.add(updatedPatterns);
useHashes = false;
} else {
//if a reduction happened, make sure hashing in reduceNestedPatterns
@@ -154,6 +157,8 @@ public class ExhaustivenessComputer {
} catch (CompletionFailure cf) {
chk.completionError(selector.pos(), cf);
return true; //error recovery
} finally {
isSubtypeCache.clear();
}
}
@@ -211,10 +216,9 @@ public class ExhaustivenessComputer {
//if a binding pattern for clazz already exists, no need to analyze it again:
!existingBindings.contains(clazz)) {
//do not reduce to types unrelated to the selector type:
Type clazzErasure = types.erasure(clazz.type);
Type clazzType = clazz.type;
if (components(selectorType).stream()
.map(types::erasure)
.noneMatch(c -> types.isSubtype(clazzErasure, c))) {
.noneMatch(c -> isSubtypeErasure(clazzType, c))) {
continue;
}
@@ -238,18 +242,18 @@ public class ExhaustivenessComputer {
//cover using bpOther:
//all types that are permitted subtypes of bpOther's type:
pendingPermitted.removeIf(pending -> types.isSubtype(types.erasure(pending.type),
types.erasure(bpOther.type)));
pendingPermitted.removeIf(pending -> isSubtypeErasure(pending.type,
bpOther.type));
if (bpOther.type.tsym.isAbstract()) {
//all types that are in a diamond hierarchy with bpOther's type
//i.e. there's a common subtype of the given type and bpOther's type:
Predicate<Symbol> check =
pending -> permitted.stream()
.filter(perm -> types.isSubtype(types.erasure(perm.type),
types.erasure(bpOther.type)))
.filter(perm -> types.isSubtype(types.erasure(perm.type),
types.erasure(pending.type)))
.filter(perm -> isSubtypeErasure(perm.type,
bpOther.type))
.filter(perm -> isSubtypeErasure(perm.type,
pending.type))
.findAny()
.isPresent();
@@ -374,30 +378,15 @@ public class ExhaustivenessComputer {
join.append(rpOne);
NEXT_PATTERN: for (int nextCandidate = 0;
nextCandidate < candidatesArr.length;
nextCandidate++) {
for (int nextCandidate = 0; nextCandidate < candidatesArr.length; nextCandidate++) {
if (firstCandidate == nextCandidate) {
continue;
}
RecordPattern rpOther = candidatesArr[nextCandidate];
if (rpOne.recordType.tsym == rpOther.recordType.tsym) {
for (int i = 0; i < rpOne.nested.length; i++) {
if (i != mismatchingCandidate) {
if (!rpOne.nested[i].equals(rpOther.nested[i])) {
if (useHashes ||
//when not using hashes,
//check if rpOne.nested[i] is
//a subtype of rpOther.nested[i]:
!(rpOne.nested[i] instanceof BindingPattern bpOne) ||
!(rpOther.nested[i] instanceof BindingPattern bpOther) ||
!types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) {
continue NEXT_PATTERN;
}
}
}
}
if (rpOne.recordType.tsym == rpOther.recordType.tsym &&
nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, useHashes)) {
join.append(rpOther);
}
}
@@ -418,9 +407,11 @@ public class ExhaustivenessComputer {
PatternDescription[] newNested =
Arrays.copyOf(rpOne.nested, rpOne.nested.length);
newNested[mismatchingCandidateFin] = nested;
current.add(new RecordPattern(rpOne.recordType(),
RecordPattern nue = new RecordPattern(rpOne.recordType(),
rpOne.fullComponentTypes(),
newNested));
newNested,
new HashSet<>(join));
current.add(nue);
}
}
}
@@ -437,6 +428,70 @@ public class ExhaustivenessComputer {
return patterns;
}
/* Returns true if all nested components of existing and candidate are
* equivalent (if useHashes == true), or "substitutable" (if useHashes == false).
* A candidate pattern is "substitutable" if it is a binding pattern, and:
* - it's type is a supertype of the existing pattern's type
* - it was produced by a reduction from a record pattern that is equivalent to
* the existing pattern
*/
private boolean nestedComponentsEquivalent(RecordPattern existing,
RecordPattern candidate,
int mismatchingCandidate,
boolean useHashes) {
NEXT_NESTED:
for (int i = 0; i < existing.nested.length; i++) {
if (i != mismatchingCandidate) {
if (!existing.nested[i].equals(candidate.nested[i])) {
if (useHashes) {
return false;
}
//when not using hashes,
//check if rpOne.nested[i] is
//a subtype of rpOther.nested[i]:
if (!(candidate.nested[i] instanceof BindingPattern nestedCandidate)) {
return false;
}
if (existing.nested[i] instanceof BindingPattern nestedExisting) {
if (!isSubtypeErasure(nestedExisting.type, nestedCandidate.type)) {
return false;
}
} else if (existing.nested[i] instanceof RecordPattern nestedExisting) {
java.util.List<PatternDescription> pendingReplacedPatterns =
new ArrayList<>(nestedCandidate.sourcePatterns());
while (!pendingReplacedPatterns.isEmpty()) {
PatternDescription currentReplaced = pendingReplacedPatterns.removeLast();
if (nestedExisting.equals(currentReplaced)) {
//candidate.nested[i] is substitutable for existing.nested[i]
//continue with the next nested pattern:
continue NEXT_NESTED;
}
pendingReplacedPatterns.addAll(currentReplaced.sourcePatterns());
}
return false;
} else {
return false;
}
}
}
}
return true;
}
/*The same as types.isSubtype(types.erasure(t), types.erasure(s)), but cached.
*/
private boolean isSubtypeErasure(Type t, Type s) {
Pair<Type, Type> key = Pair.of(t, s);
return isSubtypeCache.computeIfAbsent(key, _ ->
types.isSubtype(types.erasure(t), types.erasure(s)));
}
/* In the set of patterns, find those for which, given:
* $record($nested1, $nested2, ...)
* all the $nestedX pattern cover the given record component,
@@ -480,9 +535,11 @@ public class ExhaustivenessComputer {
covered &= checkCovered(componentType[i], List.of(newNested));
}
if (covered) {
return new BindingPattern(rpOne.recordType);
PatternDescription pd = new BindingPattern(rpOne.recordType, Set.of(pattern));
return pd;
} else if (reducedNestedPatterns != null) {
return new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns);
PatternDescription pd = new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns, Set.of(pattern));
return pd;
}
}
return pattern;
@@ -517,7 +574,9 @@ public class ExhaustivenessComputer {
return false;
}
sealed interface PatternDescription { }
sealed interface PatternDescription {
public Set<PatternDescription> sourcePatterns();
}
public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) {
if (pattern instanceof JCBindingPattern binding) {
Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type)
@@ -552,7 +611,12 @@ public class ExhaustivenessComputer {
throw Assert.error();
}
}
record BindingPattern(Type type) implements PatternDescription {
record BindingPattern(Type type, Set<PatternDescription> sourcePatterns) implements PatternDescription {
public BindingPattern(Type type) {
this(type, Set.of());
}
@Override
public int hashCode() {
return type.tsym.hashCode();
@@ -567,10 +631,14 @@ public class ExhaustivenessComputer {
return type.tsym + " _";
}
}
record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription {
record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription[] nested, Set<PatternDescription> sourcePatterns) implements PatternDescription {
public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) {
this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested);
this(recordType, fullComponentTypes, nested, Set.of());
}
public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested, Set<PatternDescription> sourcePatterns) {
this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested, sourcePatterns);
}
@Override

View File

@@ -23,7 +23,7 @@
/**
* @test
* @bug 8262891 8268871 8274363 8281100 8294670 8311038 8311815 8325215 8333169 8327368 8366968
* @bug 8262891 8268871 8274363 8281100 8294670 8311038 8311815 8325215 8333169 8327368 8366968 8364991
* @summary Check exhaustiveness of switches over sealed types.
* @library /tools/lib
* @modules jdk.compiler/com.sun.tools.javac.api
@@ -40,6 +40,7 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -1543,7 +1544,7 @@ public class Exhaustiveness extends TestRunner {
private static final int NESTING_CONSTANT = 4;
Set<String> createDeeplyNestedVariants() {
Set<String> variants = new HashSet<>();
Set<String> variants = new LinkedHashSet<>();
variants.add("C _");
variants.add("R(I _, I _, I _)");
for (int n = 0; n < NESTING_CONSTANT; n++) {
@@ -1636,10 +1637,11 @@ public class Exhaustiveness extends TestRunner {
@Test
public void testDeeplyNestedNotExhaustive(Path base) throws Exception {
List<String> variants = createDeeplyNestedVariants().stream().collect(Collectors.toCollection(ArrayList::new));
variants.remove((int) (Math.random() * variants.size()));
int removed = (int) (Math.random() * variants.size());
variants.remove(removed);
String code = testCodeForVariants(variants);
System.err.println("analyzing:");
System.err.println("analyzing (removed: " + removed + "):");
System.err.println(code);
doTest(base,
new String[0],
@@ -2311,6 +2313,180 @@ public class Exhaustiveness extends TestRunner {
"1 error");
}
@Test //JDK-8364991
public void testDifferentReductionPaths(Path base) throws Exception {
doTest(base,
new String[0],
"""
package test;
public class Test {
private int test(Root r) {
return switch (r) {
case Root(R1 _, _, _) -> 0;
case Root(R2 _, R1 _, _) -> 0;
case Root(R2 _, R2 _, R1 _) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2 _) -> 0; //functionally equivalent to: Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _))
};
}
sealed interface Base {}
record R1() implements Base {}
record R2(Base b1, Base b2) implements Base {}
record Root(Base b1, Base b2, Base b3) {}
}
""");
doTest(base,
new String[0],
"""
package test;
public class Test {
private int test(Root<Base> r) {
return switch (r) {
case Root(R1 _, _, _) -> 0;
case Root(R2 _, R1 _, _) -> 0;
case Root(R2 _, R2 _, R1 _) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R1 _), R2(R2 _, R2 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R1 _, R2 _), R2(R2 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R1 _), R2(R2 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2(R1 _, R2 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R1 _)) -> 0;
case Root(R2 _, R2(R2 _, R2 _), R2 _) -> 0; //functionally equivalent to: Root(R2 _, R2(R2 _, R2 _), R2(R2 _, R2 _))
};
}
sealed interface Base {}
record R1() implements Base {}
record R2(Base b1, Base b2) implements Base {}
record Root<T>(T b1, T b2, T b3) {}
}
""");
}
@Test //JDK-8364991
public void testDifferentReductionPathsSimplified(Path base) throws Exception {
doTest(base,
new String[0],
"""
package test;
public class Test {
private int test1(Root r) {
return switch (r) {
case Root(R2(R1 _), R2(R1 _)) -> 0;
case Root(R2(R1 _), R2(R2 _)) -> 0;
case Root(R2(R2 _), R2(R1 _)) -> 0;
case Root(R2(R2 _), R2 _) -> 0;
};
}
private int test2(Root r) {
return switch (r) {
case Root(R2(R1 _), R2(R1 _)) -> 0;
case Root(R2(R2 _), R2(R1 _)) -> 0;
case Root(R2(R1 _), R2(R2 _)) -> 0;
case Root(R2 _, R2(R2 _)) -> 0;
};
}
sealed interface Base {}
record R1() implements Base {}
record R2(Base b1) implements Base {}
record Root(R2 b2, R2 b3) {}
}
""");
doTest(base,
new String[0],
"""
package test;
public class Test {
private int test1(Root<R2<Base>> r) {
return switch (r) {
case Root(R2(R1 _), R2(R1 _)) -> 0;
case Root(R2(R1 _), R2(R2 _)) -> 0;
case Root(R2(R2 _), R2(R1 _)) -> 0;
case Root(R2(R2 _), R2 _) -> 0;
};
}
private int test2(Root<R2<Base>> r) {
return switch (r) {
case Root(R2(R1 _), R2(R1 _)) -> 0;
case Root(R2(R2 _), R2(R1 _)) -> 0;
case Root(R2(R1 _), R2(R2 _)) -> 0;
case Root(R2 _, R2(R2 _)) -> 0;
};
}
sealed interface Base {}
record R1() implements Base {}
record R2<T>(T b1) implements Base {}
record Root<T>(T b2, T b3) {}
}
""");
}
@Test //JDK-8364991
public void testBindingPatternDoesNotStandInPlaceOfRecordPatterns(Path base) throws Exception {
doTest(base,
new String[0],
"""
package test;
public class Test {
private int test(Root r) {
return switch (r) {
case Root(R2 _, R2(R1 _)) -> 0;
case Root(R2(R1 _), R2(R2 _)) -> 0;
case Root(R2(R2 _), R2 _) -> 0;
};
}
sealed interface Base {}
record R1() implements Base {}
record R2(Base b) implements Base {}
record Root(R2 b2, R2 b3) {}
}
""",
"Test.java:4:16: compiler.err.not.exhaustive",
"1 error");
doTest(base,
new String[0],
"""
package test;
public class Test {
private int test(Root<R2<Base>> r) {
return switch (r) {
case Root(R2 _, R2(R1 _)) -> 0;
case Root(R2(R1 _), R2(R2 _)) -> 0;
case Root(R2(R2 _), R2 _) -> 0;
};
}
sealed interface Base {}
record R1() implements Base {}
record R2<T>(T b) implements Base {}
record Root<T>(T b2, T b3) {}
}
""",
"Test.java:4:16: compiler.err.not.exhaustive",
"1 error");
}
private void doTest(Path base, String[] libraryCode, String testCode, String... expectedErrors) throws IOException {
doTest(base, libraryCode, testCode, false, expectedErrors);
}