diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java index bbc330832f3..2a0bab1e330 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,11 +41,17 @@ 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.Collection; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.SequencedSet; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.util.stream.Collectors.groupingBy; +import static com.sun.tools.javac.code.Flags.RECORD; /** A class to compute exhaustiveness of set of switch cases. * @@ -55,6 +61,14 @@ import static java.util.stream.Collectors.groupingBy; * deletion without notice. */ public class ExhaustivenessComputer { + private static final long DEFAULT_MAX_BASE_CHECKS = 4_000_000; + + //when baseChecks is set to a value different that this, the checks + //will be counter, and if too many will happen, the process will be stopped + //when baseChecks is set to this value, there's no counting, and the + //process will not continue as long as needed + private static final long NO_BASE_CHECKS_COUNTING = -1; + protected static final Context.Key exhaustivenessKey = new Context.Key<>(); private final Symtab syms; @@ -62,6 +76,8 @@ public class ExhaustivenessComputer { private final Check chk; private final Infer infer; private final Map, Boolean> isSubtypeCache = new HashMap<>(); + private final long maxBaseChecks; + private long baseChecks = NO_BASE_CHECKS_COUNTING; public static ExhaustivenessComputer instance(Context context) { ExhaustivenessComputer instance = context.get(exhaustivenessKey); @@ -77,9 +93,22 @@ public class ExhaustivenessComputer { types = Types.instance(context); chk = Check.instance(context); infer = Infer.instance(context); + Options options = Options.instance(context); + String baseChecks = options.get("exhaustivityMaxBaseChecks"); + long computedMaxBaseChecks = DEFAULT_MAX_BASE_CHECKS; + + if (baseChecks != null) { + try { + computedMaxBaseChecks = Long.parseLong(baseChecks); + } catch (NumberFormatException _) { + //ignore invalid values and use the default maximum number of checks + } + } + + maxBaseChecks = computedMaxBaseChecks; } - public boolean exhausts(JCExpression selector, List cases) { + public ExhaustivenessResult exhausts(JCExpression selector, List cases) { Set patternSet = new HashSet<>(); Map> enum2Constants = new HashMap<>(); Set booleanLiterals = new HashSet<>(Set.of(0, 1)); @@ -113,7 +142,7 @@ public class ExhaustivenessComputer { } if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { - return true; + return ExhaustivenessResult.ofExhaustive(); } for (Entry> e : enum2Constants.entrySet()) { @@ -121,47 +150,77 @@ public class ExhaustivenessComputer { patternSet.add(new BindingPattern(e.getKey().type)); } } - Set patterns = patternSet; - Set> seenFallback = new HashSet<>(); - boolean useHashes = true; try { - boolean repeat = true; - while (repeat) { - Set updatedPatterns; - updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - repeat = !updatedPatterns.equals(patterns); - if (checkCovered(selector.type, patterns)) { - return true; - } - if (!repeat) { - //there may be situation like: - //class B permits S1, S2 - //patterns: R(S1, B), R(S2, S2) - //this might be joined to R(B, S2), as B could be rewritten to S2 - //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 && seenFallback.add(updatedPatterns); - useHashes = false; - } else { - //if a reduction happened, make sure hashing in reduceNestedPatterns - //is enabled, as the hashing speeds up the process significantly: - useHashes = true; - } - patterns = updatedPatterns; + CoverageResult coveredResult = computeCoverage(selector.type, patternSet, PatternEquivalence.STRICT); + if (coveredResult.covered()) { + return ExhaustivenessResult.ofExhaustive(); } - return checkCovered(selector.type, patterns); + + Set details = + this.computeMissingPatternDescriptions(selector.type, coveredResult.incompletePatterns()) + .stream() + .flatMap(pd -> { + if (pd instanceof BindingPattern bp && enum2Constants.containsKey(bp.type.tsym)) { + Symbol enumType = bp.type.tsym; + return enum2Constants.get(enumType).stream().map(c -> new EnumConstantPattern(bp.type, c.name)); + } else { + return Stream.of(pd); + } + }) + .collect(Collectors.toSet()); + + return ExhaustivenessResult.ofDetails(details); } catch (CompletionFailure cf) { chk.completionError(selector.pos(), cf); - return true; //error recovery - } finally { - isSubtypeCache.clear(); + return ExhaustivenessResult.ofExhaustive(); //error recovery } } + /* Given the set of patterns, runs the reductions of it as long as possible. + * If the (reduced) set of patterns covers the given selector type, returns + * covered == true, and incompletePatterns == null. + * If the (reduced) set of patterns does not cover the given selector type, + * returns covered == false, and incompletePatterns == the reduced set of patterns. + */ + private CoverageResult computeCoverage(Type selectorType, Set patterns, PatternEquivalence patternEquivalence) { + Set updatedPatterns; + Set> seenPatterns = new HashSet<>(); + boolean useHashes = true; + boolean repeat = true; + do { + updatedPatterns = reduceBindingPatterns(selectorType, patterns); + updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes, patternEquivalence); + updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + repeat = !updatedPatterns.equals(patterns); + if (checkCovered(selectorType, patterns)) { + return new CoverageResult(true, null); + } + if (!repeat) { + //there may be situation like: + //class B permits S1, S2 + //patterns: R(S1, B), R(S2, S2) + //this might be joined to R(B, S2), as B could be rewritten to S2 + //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 && seenPatterns.add(updatedPatterns); + useHashes = false; + } else { + //if a reduction happened, make sure hashing in reduceNestedPatterns + //is enabled, as the hashing speeds up the process significantly: + useHashes = true; + } + patterns = updatedPatterns; + } while (repeat); + if (checkCovered(selectorType, patterns)) { + return new CoverageResult(true, null); + } + return new CoverageResult(false, patterns); + } + + private record CoverageResult(boolean covered, Set incompletePatterns) {} + private boolean checkCovered(Type seltype, Iterable patterns) { for (Type seltypeComponent : components(seltype)) { for (PatternDescription pd : patterns) { @@ -215,6 +274,7 @@ public class ExhaustivenessComputer { if (clazz.isSealed() && clazz.isAbstract() && //if a binding pattern for clazz already exists, no need to analyze it again: !existingBindings.contains(clazz)) { + ListBuffer bindings = new ListBuffer<>(); //do not reduce to types unrelated to the selector type: Type clazzType = clazz.type; if (components(selectorType).stream() @@ -222,16 +282,7 @@ public class ExhaustivenessComputer { continue; } - Set permitted = allPermittedSubTypes(clazz, csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(selectorType, csym); - } - - return instantiated != null && types.isCastable(selectorType, instantiated); - }); + Set permitted = allPermittedSubTypes(clazz, isApplicableSubtypePredicate(selectorType)); //the set of pending permitted subtypes needed to cover clazz: Set pendingPermitted = new HashSet<>(permitted); @@ -263,7 +314,7 @@ public class ExhaustivenessComputer { } if (pendingPermitted.isEmpty()) { - toAdd.add(new BindingPattern(clazz.type)); + toAdd.add(new BindingPattern(clazz.type, Set.of())); } } } @@ -304,6 +355,49 @@ public class ExhaustivenessComputer { return permitted; } + private Predicate isApplicableSubtypePredicate(Type targetType) { + return csym -> { + Type instantiated = instantiatePatternType(targetType, csym); + + return instantiated != null && types.isCastable(targetType, instantiated); + }; + } + + private Type instantiatePatternType(Type targetType, TypeSymbol csym) { + if (csym.type.allparams().isEmpty()) { + return csym.type; + } else { + return infer.instantiatePatternType(targetType, csym); + } + } + + private Set leafPermittedSubTypes(TypeSymbol root, Predicate accept) { + Set permitted = new HashSet<>(); + List permittedSubtypesClosure = baseClasses(root); + + while (permittedSubtypesClosure.nonEmpty()) { + ClassSymbol current = permittedSubtypesClosure.head; + + permittedSubtypesClosure = permittedSubtypesClosure.tail; + + current.complete(); + + if (current.isSealed() && current.isAbstract()) { + for (Type t : current.getPermittedSubclasses()) { + ClassSymbol csym = (ClassSymbol) t.tsym; + + if (accept.test(csym)) { + permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); + } + } + } else { + permitted.add(current); + } + } + + return permitted; + } + private List baseClasses(TypeSymbol root) { if (root instanceof ClassSymbol clazz) { return List.of(clazz); @@ -336,7 +430,8 @@ public class ExhaustivenessComputer { * as pattern hashes cannot be used to speed up the matching process */ private Set reduceNestedPatterns(Set patterns, - boolean useHashes) { + boolean useHashes, + PatternEquivalence patternEquivalence) { /* implementation note: * finding a sub-set of patterns that only differ in a single * column is time-consuming task, so this method speeds it up by: @@ -386,13 +481,13 @@ public class ExhaustivenessComputer { RecordPattern rpOther = candidatesArr[nextCandidate]; if (rpOne.recordType.tsym == rpOther.recordType.tsym && - nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, useHashes)) { + nestedComponentsEquivalent(rpOne, rpOther, mismatchingCandidate, useHashes, patternEquivalence)) { join.append(rpOther); } } var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes, patternEquivalence); updatedPatterns = reduceRecordPatterns(updatedPatterns); updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); @@ -403,16 +498,11 @@ public class ExhaustivenessComputer { current.removeAll(join); } - for (PatternDescription nested : updatedPatterns) { - PatternDescription[] newNested = - Arrays.copyOf(rpOne.nested, rpOne.nested.length); - newNested[mismatchingCandidateFin] = nested; - RecordPattern nue = new RecordPattern(rpOne.recordType(), - rpOne.fullComponentTypes(), - newNested, - new HashSet<>(join)); - current.add(nue); - } + generatePatternsWithReplacedNestedPattern(rpOne, + mismatchingCandidateFin, + updatedPatterns, + Set.copyOf(join), + current::add); } } } @@ -434,11 +524,32 @@ public class ExhaustivenessComputer { * - 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 + * - only if PatternEquivalence is LOOSE and the type is the same of the type + * of an existing record pattern (the binding pattern may stand in place of + * a record pattern). This is only used to compute the missing patterns that + * would make the original pattern set exhaustive. + * + * For example, having (with mismatchingCandidate == 0): + * existing: R(A _, Box(var _)) {} + * cadidate: R(B _, Box(var _)) {} + * these are always equivalent; as all nested patterns except of + * component 0 are exactly equivalent + * + * existing: R(A _, SubtypeOfBox _) {} + * cadidate: R(A _, Box _) {} + * this is only equivalent when useHashes == false; Box _ could be replaced + * with a more specific SubtypeOfBox _ + * + * existing: R(A _, Box(var _)) {} + * cadidate: R(A _, Box _) {} + * this is only equivalent when useHashes == false and patternEquivalence == LOOSE; + * Box _ is accepted in place of the more specific record pattern */ private boolean nestedComponentsEquivalent(RecordPattern existing, RecordPattern candidate, int mismatchingCandidate, - boolean useHashes) { + boolean useHashes, + PatternEquivalence patternEquivalence) { NEXT_NESTED: for (int i = 0; i < existing.nested.length; i++) { if (i != mismatchingCandidate) { @@ -457,22 +568,28 @@ public class ExhaustivenessComputer { return false; } } else if (existing.nested[i] instanceof RecordPattern nestedExisting) { - java.util.List pendingReplacedPatterns = - new ArrayList<>(nestedCandidate.sourcePatterns()); + if (patternEquivalence == PatternEquivalence.LOOSE) { + if (!isSubtypeErasure(nestedExisting.recordType(), nestedCandidate.type)) { + return false; + } + } else { + java.util.List pendingReplacedPatterns = + new ArrayList<>(nestedCandidate.sourcePatterns()); - while (!pendingReplacedPatterns.isEmpty()) { - PatternDescription currentReplaced = pendingReplacedPatterns.removeLast(); + 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; + 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()); } - pendingReplacedPatterns.addAll(currentReplaced.sourcePatterns()); + return false; } - - return false; } else { return false; } @@ -563,6 +680,8 @@ public class ExhaustivenessComputer { } private boolean isBpCovered(Type componentType, PatternDescription newNested) { + reportCheck(); + if (newNested instanceof BindingPattern bp) { Type seltype = types.erasure(componentType); Type pattype = types.erasure(bp.type); @@ -574,9 +693,18 @@ public class ExhaustivenessComputer { return false; } - sealed interface PatternDescription { + protected void reportCheck() { + if (baseChecks != NO_BASE_CHECKS_COUNTING && + ++baseChecks > maxBaseChecks) { + throw new TooManyChecksException(null); + } + } + + public sealed interface PatternDescription { + public Type type(); public Set sourcePatterns(); } + public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { if (pattern instanceof JCBindingPattern binding) { Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) @@ -586,9 +714,7 @@ public class ExhaustivenessComputer { Type[] componentTypes; if (!record.type.isErroneous()) { - componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() - .map(r -> types.memberType(record.type, r)) - .toArray(s -> new Type[s]); + componentTypes = instantiatedComponentTypes(record.type); } else { componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; @@ -611,7 +737,7 @@ public class ExhaustivenessComputer { throw Assert.error(); } } - record BindingPattern(Type type, Set sourcePatterns) implements PatternDescription { + public record BindingPattern(Type type, Set sourcePatterns) implements PatternDescription { public BindingPattern(Type type) { this(type, Set.of()); @@ -631,7 +757,7 @@ public class ExhaustivenessComputer { return type.tsym + " _"; } } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription[] nested, Set sourcePatterns) implements PatternDescription { + public record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription[] nested, Set sourcePatterns) implements PatternDescription { public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { this(recordType, fullComponentTypes, nested, Set.of()); @@ -673,5 +799,450 @@ public class ExhaustivenessComputer { .map(pd -> pd.toString()) .collect(Collectors.joining(", ")) + ")"; } + + @Override + public Type type() { + return recordType; + } + } + + public record EnumConstantPattern(Type enumType, Name enumConstant) implements PatternDescription { + + @Override + public Type type() { + return enumType(); + } + + @Override + public Set sourcePatterns() { + return Set.of(); + } + public String toString() { + return enumType() + "." + enumConstant(); + } + } + + public record ExhaustivenessResult(boolean exhaustive, Set notExhaustiveDetails) { + public static ExhaustivenessResult ofExhaustive() { + return new ExhaustivenessResult(true, null); + } + public static ExhaustivenessResult ofDetails(Set notExhaustiveDetails) { + return new ExhaustivenessResult(false, notExhaustiveDetails != null ? notExhaustiveDetails : Set.of()); + } + } + + //computation of missing patterns: + protected Set computeMissingPatternDescriptions(Type selectorType, + Set incompletePatterns) { + if (maxBaseChecks == 0) { + return Set.of(); + } + try { + baseChecks = 0; + PatternDescription defaultPattern = new BindingPattern(selectorType); + return expandMissingPatternDescriptions(selectorType, + selectorType, + defaultPattern, + incompletePatterns, + Set.of(defaultPattern)); + } catch (TooManyChecksException ex) { + return ex.missingPatterns != null ? ex.missingPatterns : Set.of(); + } finally { + baseChecks = NO_BASE_CHECKS_COUNTING; + } + } + + private Set expandMissingPatternDescriptions(Type selectorType, + Type targetType, + PatternDescription toExpand, + Set basePatterns, + Set inMissingPatterns) { + try { + return doExpandMissingPatternDescriptions(selectorType, targetType, + toExpand, basePatterns, + inMissingPatterns); + } catch (TooManyChecksException ex) { + if (ex.missingPatterns == null) { + ex = new TooManyChecksException(inMissingPatterns); + } + throw ex; + } + } + + private Set doExpandMissingPatternDescriptions(Type selectorType, + Type targetType, + PatternDescription toExpand, + Set basePatterns, + Set inMissingPatterns) { + if (toExpand instanceof BindingPattern bp) { + if (bp.type.tsym.isSealed()) { + //try to replace binding patterns for sealed types with all their immediate permitted applicable types: + List permitted = ((ClassSymbol) bp.type.tsym).getPermittedSubclasses(); + Set applicableDirectPermittedPatterns = + permitted.stream() + .map(type -> type.tsym) + .filter(isApplicableSubtypePredicate(targetType)) + .map(csym -> new BindingPattern(types.erasure(csym.type))) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + //remove the permitted subtypes that are not needed to achieve exhaustivity + boolean reduced = + removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, applicableDirectPermittedPatterns); + + if (!reduced && !hasMatchingRecordPattern(basePatterns, inMissingPatterns, toExpand)) { + //if all immediate permitted subtypes are needed + //give up, and simply use the current pattern: + return inMissingPatterns; + } + + Set currentMissingPatterns = + replace(inMissingPatterns, toExpand, applicableDirectPermittedPatterns); + + //try to recursively expand on each viable pattern: + for (PatternDescription viable : applicableDirectPermittedPatterns) { + currentMissingPatterns = expandMissingPatternDescriptions(selectorType, targetType, + viable, basePatterns, + currentMissingPatterns); + } + + return currentMissingPatterns; + } else if ((bp.type.tsym.flags_field & Flags.RECORD) != 0 && + //only expand record types into record patterns if there's a chance it may change the outcome + //i.e. there is a record pattern in at the spot in the original base patterns: + hasMatchingRecordPattern(basePatterns, inMissingPatterns, toExpand)) { + //if there is a binding pattern at a place where the original based patterns + //have a record pattern, try to expand the binding pattern into a record pattern + //create all possible combinations of record pattern components: + Type[] componentTypes = instantiatedComponentTypes(bp.type); + List> combinatorialNestedTypes = List.of(List.nil()); + + for (Type componentType : componentTypes) { + List applicableLeafPermittedSubtypes; + + if (componentType.tsym.isSealed()) { + applicableLeafPermittedSubtypes = + leafPermittedSubTypes(componentType.tsym, + isApplicableSubtypePredicate(componentType)) + .stream() + .map(csym -> instantiatePatternType(componentType, csym)) + .collect(List.collector()); + } else { + applicableLeafPermittedSubtypes = List.of(componentType); + } + + List> newCombinatorialNestedTypes = List.nil(); + + for (List existing : combinatorialNestedTypes) { + for (Type nue : applicableLeafPermittedSubtypes) { + newCombinatorialNestedTypes = newCombinatorialNestedTypes.prepend(existing.append(nue)); + } + } + + combinatorialNestedTypes = newCombinatorialNestedTypes; + } + + Set combinatorialPatterns = + combinatorialNestedTypes.stream() + .map(combination -> new RecordPattern(bp.type, + componentTypes, + combination.map(BindingPattern::new) + .toArray(PatternDescription[]::new))) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, combinatorialPatterns); + + CoverageResult coverageResult = computeCoverage(targetType, combinatorialPatterns, PatternEquivalence.LOOSE); + + if (!coverageResult.covered()) { + //use the partially merged/combined patterns: + combinatorialPatterns = coverageResult.incompletePatterns(); + } + + //combine sealed subtypes into the supertype, if all is covered. + //but preserve more specific record types in positions where there are record patterns in the original patterns + //this is particularly important for the case where the sealed supertype only has one permitted type, the record + //the base type could be used instead of the record otherwise, which would produce less specific missing pattern: + Set sortedCandidates = + partialSortPattern(combinatorialPatterns, basePatterns, replace(inMissingPatterns, toExpand, combinatorialPatterns)); + + removeUnnecessaryPatterns(selectorType, bp, basePatterns, inMissingPatterns, sortedCandidates); + + Set currentMissingPatterns = + replace(inMissingPatterns, toExpand, sortedCandidates); + + for (PatternDescription addedPattern : sortedCandidates) { + if (addedPattern instanceof RecordPattern addedRP) { + for (int c = 0; c < addedRP.nested.length; c++) { + currentMissingPatterns = expandMissingPatternDescriptions(selectorType, + addedRP.fullComponentTypes[c], + addedRP.nested[c], + basePatterns, + currentMissingPatterns); + } + } + } + + return currentMissingPatterns; + } + } + return inMissingPatterns; + } + + /* + * Inside any pattern in {@code in}, in any nesting depth, replace + * pattern {@code what} with patterns {@code to}. + */ + private Set replace(Iterable in, + PatternDescription what, + Collection to) { + Set result = new HashSet<>(); + + for (PatternDescription pd : in) { + Collection replaced = replace(pd, what, to); + if (replaced != null) { + result.addAll(replaced); + } else { + result.add(pd); + } + } + + return result; + } + //where: + //null: no change + private Collection replace(PatternDescription in, + PatternDescription what, + Collection to) { + if (in == what) { + return to; + } else if (in instanceof RecordPattern rp) { + for (int c = 0; c < rp.nested.length; c++) { + Collection replaced = replace(rp.nested[c], what, to); + if (replaced != null) { + Set withReplaced = new HashSet<>(); + + generatePatternsWithReplacedNestedPattern(rp, c, replaced, Set.of(), withReplaced::add); + + return replace(withReplaced, what, to); + } + } + return null; + } else { + return null; //binding patterns have no children + } + } + + /* Out of "candidates" remove patterns that are not necessary to achieve exhaustiveness. + * Note that iteration order of "candidates" is important - if the set contains + * two pattern, out of which either, but not both, is needed to achieve exhaustiveness, + * the first one in the iteration order will be removed. + */ + private boolean removeUnnecessaryPatterns(Type selectorType, + PatternDescription toExpand, + Set basePatterns, + Set inMissingPatterns, + Set candidates) { + boolean reduced = false; + + for (Iterator it = candidates.iterator(); it.hasNext(); ) { + PatternDescription current = it.next(); + Set reducedAdded = new HashSet<>(candidates); + + reducedAdded.remove(current); + + Set combinedPatterns = + Stream.concat(basePatterns.stream(), + replace(inMissingPatterns, toExpand, reducedAdded).stream()) + .collect(Collectors.toSet()); + + if (computeCoverage(selectorType, combinedPatterns, PatternEquivalence.LOOSE).covered()) { + it.remove(); + reduced = true; + } + } + + return reduced; + } + /* + * Sort patterns so that those that are preferred for removal are in front + * of those that are preferred to remain (when there's a choice). + */ + private SequencedSet partialSortPattern(Set candidates, + Set basePatterns, + Set missingPatterns) { + SequencedSet sortedCandidates = new LinkedHashSet<>(); + + while (!candidates.isEmpty()) { + PatternDescription mostSpecific = null; + for (PatternDescription current : candidates) { + if (mostSpecific == null || + shouldAppearBefore(current, mostSpecific, basePatterns, missingPatterns)) { + mostSpecific = current; + } + } + sortedCandidates.add(mostSpecific); + candidates.remove(mostSpecific); + } + return sortedCandidates; + } + //where: + //true iff pd1 should appear before pd2 + //false otherwise + private boolean shouldAppearBefore(PatternDescription pd1, + PatternDescription pd2, + Set basePatterns, + Set missingPatterns) { + if (pd1 instanceof RecordPattern rp1 && pd2 instanceof RecordPattern rp2) { + for (int c = 0; c < rp1.nested.length; c++) { + if (shouldAppearBefore((BindingPattern) rp1.nested[c], + (BindingPattern) rp2.nested[c], + basePatterns, + missingPatterns)) { + return true; + } + } + } else if (pd1 instanceof BindingPattern bp1 && pd2 instanceof BindingPattern bp2) { + Type t1 = bp1.type(); + Type t2 = bp2.type(); + boolean t1IsImportantRecord = + (t1.tsym.flags_field & RECORD) != 0 && + hasMatchingRecordPattern(basePatterns, missingPatterns, bp1); + boolean t2IsImportantRecord = + (t2.tsym.flags_field & RECORD) != 0 && + hasMatchingRecordPattern(basePatterns, missingPatterns, bp2); + if (t1IsImportantRecord && !t2IsImportantRecord) { + return false; + } + if (!t1IsImportantRecord && t2IsImportantRecord) { + return true; + } + if (!types.isSameType(t1, t2) && types.isSubtype(t1, t2)) { + return true; + } + } + + return false; + } + + /* + * Do the {@code basePatterns} have a record pattern at a place that corresponds to + * position of pattern {@code query} inside {@code missingPatterns}? + */ + private boolean hasMatchingRecordPattern(Set basePatterns, + Set missingPatterns, + PatternDescription query) { + PatternDescription root = findRootContaining(missingPatterns, query); + + if (root == null) { + return false; + } + return basePatternsHaveRecordPatternOnThisSpot(basePatterns, root, query); + } + //where: + private PatternDescription findRootContaining(Set rootPatterns, + PatternDescription added) { + for (PatternDescription pd : rootPatterns) { + if (isUnderRoot(pd, added)) { + return pd; + } + } + + return null; + } + + private boolean basePatternsHaveRecordPatternOnThisSpot(Set basePatterns, + PatternDescription rootPattern, + PatternDescription added) { + if (rootPattern == added) { + return basePatterns.stream().anyMatch(pd -> pd instanceof RecordPattern); + } + if (!(rootPattern instanceof RecordPattern rootPatternRecord)) { + return false; + } + int index = -1; + for (int c = 0; c < rootPatternRecord.nested.length; c++) { + if (isUnderRoot(rootPatternRecord.nested[c], added)) { + index = c; + break; + } + } + // 'index' must be one of rootPatternRecord.nested; if not, `isUnderRoot` is inconsistent. + Assert.check(index != (-1)); + + int indexFin = index; + Set filteredBasePatterns = + basePatterns.stream() + .filter(pd -> pd instanceof RecordPattern) + .map(rp -> (RecordPattern) rp) + .filter(rp -> types.isSameType(rp.recordType(), rootPatternRecord.recordType())) + .map(rp -> rp.nested[indexFin]) + .collect(Collectors.toSet()); + + return basePatternsHaveRecordPatternOnThisSpot(filteredBasePatterns, rootPatternRecord.nested[index], added); + } + + private boolean isUnderRoot(PatternDescription root, PatternDescription searchFor) { + if (root == searchFor) { + return true; + } else if (root instanceof RecordPattern rp) { + for (int c = 0; c < rp.nested.length; c++) { + if (isUnderRoot(rp.nested[c], searchFor)) { + return true; + } + } + } + return false; + } + + /* + * Using {@code basePattern} as a starting point, generate new {@code + * RecordPattern}s, such that all corresponding components but one, are the + * same. The component described by the {@code replaceComponent} index is + * replaced with all {@code PatternDescription}s taken from {@code + * updatedNestedPatterns} and the resulting {@code RecordPatterns}s are sent + * to {@code target}. + */ + private void generatePatternsWithReplacedNestedPattern(RecordPattern basePattern, + int replaceComponent, + Iterable updatedNestedPatterns, + Set sourcePatterns, + Consumer target) { + for (PatternDescription nested : updatedNestedPatterns) { + PatternDescription[] newNested = + Arrays.copyOf(basePattern.nested, basePattern.nested.length); + newNested[replaceComponent] = nested; + target.accept(new RecordPattern(basePattern.recordType(), + basePattern.fullComponentTypes(), + newNested, + sourcePatterns)); + } + } + + /* For a given record type, return the record's component types, with their + * types instatiated according to the exact record type. + */ + private Type[] instantiatedComponentTypes(Type recordType) { + Type[] componentTypes = ((ClassSymbol) recordType.tsym).getRecordComponents() + .map(r -> types.memberType(recordType, r)) + .toArray(s -> new Type[s]); + return componentTypes; + } + + /* The strictness of determining the equivalent of patterns, used in + * nestedComponentsEquivalent. + */ + private enum PatternEquivalence { + STRICT, + LOOSE; + } + + protected static class TooManyChecksException extends RuntimeException { + private static final long serialVersionUID = 0L; + private transient final Set missingPatterns; + + public TooManyChecksException(Set missingPatterns) { + super(null, null, false, false); + this.missingPatterns = missingPatterns; + } } } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index e74aed6a357..cbcb474a37f 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -50,9 +50,15 @@ import static com.sun.tools.javac.code.Flags.BLOCK; import static com.sun.tools.javac.code.Kinds.Kind.*; import static com.sun.tools.javac.code.TypeTag.BOOLEAN; import static com.sun.tools.javac.code.TypeTag.VOID; +import com.sun.tools.javac.comp.ExhaustivenessComputer.BindingPattern; +import com.sun.tools.javac.comp.ExhaustivenessComputer.EnumConstantPattern; +import com.sun.tools.javac.comp.ExhaustivenessComputer.ExhaustivenessResult; +import com.sun.tools.javac.comp.ExhaustivenessComputer.PatternDescription; +import com.sun.tools.javac.comp.ExhaustivenessComputer.RecordPattern; import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; +import java.util.Arrays; /** This pass implements dataflow analysis for Java programs though * different AST visitor steps. Liveness analysis (see AliveAnalyzer) checks that @@ -696,9 +702,18 @@ public class Flow { tree.isExhaustive = tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { - tree.isExhaustive |= exhaustiveness.exhausts(tree.selector, tree.cases); if (!tree.isExhaustive) { - log.error(tree, Errors.NotExhaustiveStatement); + ExhaustivenessResult exhaustivenessResult = exhaustiveness.exhausts(tree.selector, tree.cases); + + tree.isExhaustive = exhaustivenessResult.exhaustive(); + + if (!tree.isExhaustive) { + if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { + log.error(tree, Errors.NotExhaustiveStatement); + } else { + logNotExhaustiveError(tree.pos(), exhaustivenessResult, Errors.NotExhaustiveStatementDetails); + } + } } } if (!tree.hasUnconditionalPattern && !exhaustiveSwitch) { @@ -735,16 +750,54 @@ public class Flow { TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) { tree.isExhaustive = true; } else { - tree.isExhaustive = exhaustiveness.exhausts(tree.selector, tree.cases); + ExhaustivenessResult exhaustivenessResult = exhaustiveness.exhausts(tree.selector, tree.cases); + + tree.isExhaustive = exhaustivenessResult.exhaustive(); + + if (!tree.isExhaustive) { + if (exhaustivenessResult.notExhaustiveDetails().isEmpty()) { + log.error(tree, Errors.NotExhaustive); + } else { + logNotExhaustiveError(tree.pos(), exhaustivenessResult, Errors.NotExhaustiveDetails); + } + } } - if (!tree.isExhaustive) { - log.error(tree, Errors.NotExhaustive); - } alive = prevAlive; alive = alive.or(resolveYields(tree, prevPendingExits)); } + private void logNotExhaustiveError(DiagnosticPosition pos, + ExhaustivenessResult exhaustivenessResult, + Error errorKey) { + List details = + exhaustivenessResult.notExhaustiveDetails() + .stream() + .map(this::patternToDiagnostic) + .sorted((d1, d2) -> d1.toString() + .compareTo(d2.toString())) + .collect(List.collector()); + JCDiagnostic main = diags.error(null, log.currentSource(), pos, errorKey); + JCDiagnostic d = new JCDiagnostic.MultilineDiagnostic(main, details); + log.report(d); + } + + private JCDiagnostic patternToDiagnostic(PatternDescription desc) { + Type patternType = types.erasure(desc.type()); + return diags.fragment(switch (desc) { + case BindingPattern _ -> + Fragments.BindingPattern(patternType); + case RecordPattern rp -> + Fragments.RecordPattern(patternType, + Arrays.stream(rp.nested()) + .map(this::patternToDiagnostic) + .toList()); + case EnumConstantPattern ep -> + Fragments.EnumConstantPattern(patternType, + ep.enumConstant()); + }); + } + public void visitTry(JCTry tree) { ListBuffer prevPendingExits = pendingExits; pendingExits = new ListBuffer<>(); diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties index 2ba9122c04a..bb81916becb 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -1476,6 +1476,26 @@ compiler.err.not.exhaustive=\ compiler.err.not.exhaustive.statement=\ the switch statement does not cover all possible input values +compiler.err.not.exhaustive.details=\ + the switch expression does not cover all possible input values\n\ + missing patterns: + +compiler.err.not.exhaustive.statement.details=\ + the switch statement does not cover all possible input values\n\ + missing patterns: + +# 0: type +compiler.misc.binding.pattern=\ + {0} _ + +# 0: type, 1: list of diagnostic +compiler.misc.record.pattern=\ + {0}({1}) + +# 0: type, 1: name +compiler.misc.enum.constant.pattern=\ + {0}.{1} + compiler.err.initializer.must.be.able.to.complete.normally=\ initializer must be able to complete normally diff --git a/test/langtools/tools/javac/diags/examples/BindingPattern.java b/test/langtools/tools/javac/diags/examples/BindingPattern.java new file mode 100644 index 00000000000..01913a47fd7 --- /dev/null +++ b/test/langtools/tools/javac/diags/examples/BindingPattern.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +// key: compiler.err.not.exhaustive.details +// key: compiler.misc.binding.pattern + +class BindingPattern { + int t(Object o) { + return switch (o) { + case String s -> 0; + }; + } +} diff --git a/test/langtools/tools/javac/diags/examples/EnumConstantPattern.java b/test/langtools/tools/javac/diags/examples/EnumConstantPattern.java new file mode 100644 index 00000000000..b2ab5b9028d --- /dev/null +++ b/test/langtools/tools/javac/diags/examples/EnumConstantPattern.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +// key: compiler.err.not.exhaustive.details +// key: compiler.misc.enum.constant.pattern + +class NotExhaustiveDetails { + int t(I i) { + return switch (i) { + case R r -> -1; + case E.A -> -1; + }; + } + sealed interface I {} + enum E implements I {A, B} + record R(E e) implements I {} +} diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustive.java b/test/langtools/tools/javac/diags/examples/NotExhaustive.java index 8d36b013677..f048755ec79 100644 --- a/test/langtools/tools/javac/diags/examples/NotExhaustive.java +++ b/test/langtools/tools/javac/diags/examples/NotExhaustive.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,6 +22,7 @@ */ // key: compiler.err.not.exhaustive +// options: -XDexhaustivityMaxBaseChecks=0 class NotExhaustive { int t(int i) { diff --git a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java index 65a11abb0e6..49e4955cbdb 100644 --- a/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java +++ b/test/langtools/tools/javac/diags/examples/NotExhaustiveStatement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,6 +22,7 @@ */ // key: compiler.err.not.exhaustive.statement +// options: -XDexhaustivityMaxBaseChecks=0 class NotExhaustive { void t(Object o) { diff --git a/test/langtools/tools/javac/diags/examples/RecordPattern.java b/test/langtools/tools/javac/diags/examples/RecordPattern.java new file mode 100644 index 00000000000..2bb43bfd800 --- /dev/null +++ b/test/langtools/tools/javac/diags/examples/RecordPattern.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +// key: compiler.err.not.exhaustive.statement.details +// key: compiler.misc.record.pattern + +class RecordPattern { + void t(R r) { + switch (r) { + case R(C1 _) -> {} + }; + } + sealed interface I {} + record C1() implements I {} + record C2() implements I {} + record R(I i) {} +} diff --git a/test/langtools/tools/javac/patterns/Exhaustiveness.java b/test/langtools/tools/javac/patterns/Exhaustiveness.java index 84e67855b3b..f1809e99e45 100644 --- a/test/langtools/tools/javac/patterns/Exhaustiveness.java +++ b/test/langtools/tools/javac/patterns/Exhaustiveness.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -2523,6 +2523,7 @@ public class Exhaustiveness extends TestRunner { "-Xlint:-preview", "--class-path", libClasses.toString(), "-XDshould-stop.at=FLOW", + "-XDexhaustivityMaxBaseChecks=0", stopAtFlow ? "-XDshould-stop.ifNoError=FLOW" : "-XDnoop") .outdir(classes) diff --git a/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java new file mode 100644 index 00000000000..6935fcfa006 --- /dev/null +++ b/test/langtools/tools/javac/patterns/ExhaustivenessConvenientErrors.java @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8367530 + * @summary Check enhanced exhaustiveness errors + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.util + * @build toolbox.ToolBox toolbox.JavacTask + * @run main ExhaustivenessConvenientErrors +*/ + +import com.sun.tools.javac.api.ClientCodeWrapper.DiagnosticSourceUnwrapper; +import com.sun.tools.javac.util.JCDiagnostic; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import toolbox.JavacTask; +import toolbox.Task; +import toolbox.TestRunner; +import toolbox.ToolBox; + +public class ExhaustivenessConvenientErrors extends TestRunner { + + ToolBox tb; + + public static void main(String... args) throws Exception { + new ExhaustivenessConvenientErrors().runTests(); + } + + ExhaustivenessConvenientErrors() { + super(System.err); + tb = new ToolBox(); + } + + public void runTests() throws Exception { + runTests(m -> new Object[] { Paths.get(m.getName()) }); + } + + @Test + public void testExhaustiveSealedClasses(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 final class B implements S {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(S obj) { + return switch (obj) { + case A a -> 0; + }; + } + } + """, + "lib.B _"); + } + + @Test + public void testExhaustiveSealedClassesTransitive(Path base) throws Exception { + doTest(base, + new String[]{""" + package lib; + public sealed interface S1 permits S2, A {} + """, + """ + package lib; + public sealed interface S2 extends S1 permits S3, B {} + """, + """ + package lib; + public sealed interface S3 extends S2 permits C, D {} + """, + """ + package lib; + public final class A implements S1 {} + """, + """ + package lib; + public final class B implements S2 {} + """, + """ + package lib; + public final class C implements S3 {} + """, + """ + package lib; + public final class D implements S3 {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(S1 obj) { + return switch (obj) { + case A a -> 0; + case B a -> 0; + case D a -> 0; + }; + } + } + """, + "lib.C _"); + } + + @Test + public void testTrivialRecord(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 final class B implements S {} + """, + """ + package lib; + public record R(S s) {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(R r) { + return switch (r) { + case R(A a) -> 0; + }; + } + } + """, + "lib.R(lib.B _)"); + } + + @Test + public void testNonNestedRecord(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 final class B implements S {} + """, + """ + package lib; + public record R(S s1, S s2) {} + """}, + """ + package test; + import lib.*; + public class Test { + private int test(R r) { + return switch (r) { + case R(A a, B b) -> 0; + case R(B b, A a) -> 0; + }; + } + } + """, + "lib.R(lib.A _,lib.A _)", + "lib.R(lib.B _,lib.B _)"); + } + + @Test + public void testComplex1(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + import lib.*; + public class Test { + private int test(Root r) { + return switch (r) { + case Root(R1 _, _, _) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2() implements Base {} + record R3(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """, + "test.Test.Root(test.Test.R2 _,test.Test.Base _,test.Test.Base _)", + "test.Test.Root(test.Test.R3 _,test.Test.Base _,test.Test.Base _)"); + } + + @Test + public void testComplex2(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + import lib.*; + 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(R2 _, R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """, + "test.Test.Root(test.Test.R2 _,test.Test.R2(test.Test.R2 _,test.Test.R2 _),test.Test.R2(test.Test.R2 _,test.Test.R2 _))"); + } + + @Test + public void testComplex3(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Triple p) { + return switch (p) { + case Triple(B _, _, _) -> 0; + case Triple(_, A _, _) -> 0; + case Triple(_, _, A _) -> 0; + case Triple(A p, C(Nested _, NestedBaseA _), _) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; +// case Path(A p, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; + }; + } + record Triple(Base c1, Base c2, Base c3) {} + sealed interface Base permits A, B {} + record A(boolean key) implements Base { + } + sealed interface B extends Base {} + record C(Nested n, NestedBase b) implements B {} + record Nested() {} + sealed interface NestedBase {} + record NestedBaseA() implements NestedBase {} + record NestedBaseB() implements NestedBase {} + record NestedBaseC() implements NestedBase {} + } + """, + "test.Test.Triple(test.Test.A _,test.Test.C(test.Test.Nested _,test.Test.NestedBaseC _),test.Test.C(test.Test.Nested _,test.Test.NestedBaseC _))"); + } + + @Test + public void testComplex4(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + import lib.*; + 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(R2 _, R2 _)) -> 0; + }; + } + sealed interface Base {} + record R1() implements Base {} + record R2(Base b1, Base b2) implements Base {} + record Root(Base b1, Base b2, Base b3) {} + } + """, + "test.Test.Root(test.Test.R2 _,test.Test.R2(test.Test.Base _,test.Test.R2 _),test.Test.R2(test.Test.R2 _,test.Test.Base _))"); + //ideally,the result would be as follow,but it is difficult to split Base on two distinct places: +// "test.Test.Root(test.Test.R2 _,test.Test.R2(test.Test.R1 _,test.Test.R2 _),test.Test.R2(test.Test.R2 _,test.Test.R1 _))", +// "test.Test.Root(test.Test.R2 _,test.Test.R2(test.Test.R2 _,test.Test.R2 _),test.Test.R2(test.Test.R2 _,test.Test.R2 _))"); + } + + @Test + public void testComplex5(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Triple p) { + return switch (p) { + case Triple(B _, _, _) -> 0; + case Triple(_, A _, _) -> 0; + case Triple(_, _, A _) -> 0; +// case Triple(A _, C(Nested _, NestedBaseA _), _) -> 0; + case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseB _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseB _), C(Nested _, NestedBaseC _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseA _)) -> 0; + case Triple(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseB _)) -> 0; +// case Path(A _, C(Nested _, NestedBaseC _), C(Nested _, NestedBaseC _)) -> 0; + }; + } + record Triple(Base c1, Base c2, Base c3) {} + sealed interface Base permits A, B {} + record A(boolean key) implements Base { + } + sealed interface B extends Base {} + record C(Nested n, NestedBase b) implements B {} + record Nested() {} + sealed interface NestedBase {} + record NestedBaseA() implements NestedBase {} + record NestedBaseB() implements NestedBase {} + record NestedBaseC() implements NestedBase {} + } + """, + "test.Test.Triple(test.Test.A _,test.Test.C(test.Test.Nested _,test.Test.NestedBaseA _),test.Test.C _)", + //the following could be: + //test.Test.Triple(test.Test.A _,test.Test.C(test.Test.Nested _,test.Test.NestedBaseC _),test.Test.C(test.Test.Nested _,test.Test.NestedBaseC _)) + "test.Test.Triple(test.Test.A _,test.Test.C(test.Test.Nested _,test.Test.NestedBaseC _),test.Test.C _)"); + } + + @Test + public void testNoInfiniteRecursion(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(R r) { + return switch (r) { + case R(_, _, R(_, _, _, _), String s) -> 0; + case R(_, _, R(_, _, _, String str), _) -> 0; + }; + } + } + public record R(R r1, R r2, R r3, Object o) {} + """, + "test.R(test.R _,test.R _,test.R(test.R _,test.R _,test.R _,java.lang.Object _),java.lang.Object _)"); + } + + @Test + public void testEnum(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(I i) { + return switch (i) { + case E.A -> 0; + case C _ -> 1; + }; + } + sealed interface I {} + enum E implements I {A, B} + final class C implements I {} + } + public record R(R r1, R r2, R r3, Object o) {} + """, + "test.Test.E.B"); + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(I i) { + return switch (i) { + case C _ -> 1; + }; + } + sealed interface I {} + enum E implements I {A, B} + final class C implements I {} + } + public record R(R r1, R r2, R r3, Object o) {} + """, + "test.Test.E _"); + } + + @Test + public void testInstantiateComponentTypes(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + public class Test { + private int test(Pair> p) { + return switch (p) { + case Pair(A(A(_)) -> 0; + case Pair(A(B(_)) -> 0; + case Pair(B(A(_)) -> 0; + }; + } + record Pair(T c) {} + sealed interface Base permits A, B {} + record A(T c) implements Base {} + record B(T c) implements Base {} + } + """, + "test.Test.Pair(test.Test.B(test.Test.B _))"); + } + + @Test + public void testNeedToExpandIfRecordExists(Path base) throws Exception { + doTest(base, + new String[0], + """ + package test; + class Test { + sealed interface A { } + record B() implements A { } + record C(A a) implements A { } + + void test(A a) { + switch (a) { + case C(B _) -> throw null; + } + } + } """, + "test.Test.B _", + "test.Test.C(test.Test.C _)"); + } + + @Test + public void testComplex6(Path base) throws Exception { + doTest(base, + new String[0], + """ + public class Test { + sealed interface Base {} + record NoOp() implements Base {} + record Const() implements Base {} + record Pair(Base n1, + Base b2) implements Base {} + + int t(Base b) { + return switch (b) { + case NoOp _ -> 0; + case Const _ -> 0; + case Pair(NoOp _, _) -> 0; + case Pair(Const _, _) -> 0; + case Pair(Pair _, NoOp _) -> 0; + case Pair(Pair _, Const _) -> 0; + case Pair(Pair _, Pair(NoOp _, _)) -> 0; + case Pair(Pair _, Pair(Const _, _)) -> 0; + case Pair(Pair _, Pair(Pair(NoOp _, _), _)) -> 0; + case Pair(Pair _, Pair(Pair(Const _, _), _)) -> 0; + case Pair(Pair(NoOp _, _), Pair(Pair(Pair _, _), _)) -> 0; + case Pair(Pair(Const _, _), Pair(Pair(Pair _, _), _)) -> 0; +// case Pair(Pair(Pair _, _), Pair(Pair(Pair _, _), _)) -> 0; + }; + } + } + """, + "Test.Pair(Test.Pair(Test.Pair _,Test.Base _),Test.Pair(Test.Pair(Test.Pair _,Test.Base _),Test.Base _))"); + } + + private void doTest(Path base, String[] libraryCode, String testCode, String... expectedMissingPatterns) throws IOException { + Path current = base.resolve("."); + Path libClasses = current.resolve("libClasses"); + + Files.createDirectories(libClasses); + + if (libraryCode.length != 0) { + Path libSrc = current.resolve("lib-src"); + + for (String code : libraryCode) { + tb.writeJavaFiles(libSrc, code); + } + + new JavacTask(tb) + .outdir(libClasses) + .files(tb.findJavaFiles(libSrc)) + .run(); + } + + Path src = current.resolve("src"); + tb.writeJavaFiles(src, testCode); + + Path classes = current.resolve("libClasses"); + + Files.createDirectories(libClasses); + Set missingPatterns = new HashSet<>(); + + new JavacTask(tb) + .options("-XDrawDiagnostics", + "--class-path", libClasses.toString(), + "-XDshould-stop.at=FLOW", + "-XDshould-stop.ifNoError=FLOW", + "-XDexhaustivityMaxBaseChecks=" + Long.MAX_VALUE) //never give up + .outdir(classes) + .files(tb.findJavaFiles(src)) + .diagnosticListener(d -> { + if ("compiler.err.not.exhaustive.details".equals(d.getCode()) || + "compiler.err.not.exhaustive.statement.details".equals(d.getCode())) { + if (d instanceof DiagnosticSourceUnwrapper uw) { + d = uw.d; + } + if (d instanceof JCDiagnostic.MultilineDiagnostic diag) { + diag.getSubdiagnostics() + .stream() + .map(fragment -> fragment.toString()) + .forEach(missingPatterns::add); + } + } + }) + .run(Task.Expect.FAIL) + .writeAll(); + + Set expectedPatterns = new HashSet<>(List.of(expectedMissingPatterns)); + + if (!expectedPatterns.equals(missingPatterns)) { + throw new AssertionError("Incorrect errors, expected: " + expectedPatterns + + ", actual: " + missingPatterns); + } + } + +} diff --git a/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java b/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java index 82064bd4baf..1830afeb187 100644 --- a/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java +++ b/test/langtools/tools/javac/patterns/PrimitiveInstanceOfComboTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -103,16 +103,19 @@ public class PrimitiveInstanceOfComboTest extends ComboInstance { diff --git a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java index 2890b315e62..249cdeb2464 100644 --- a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java +++ b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchConstants.java @@ -2,7 +2,7 @@ * @test /nodynamiccopyright/ * @summary Retain exhaustiveness properties of switches with a constant selector * @enablePreview - * @compile/fail/ref=PrimitivePatternsSwitchConstants.out -XDrawDiagnostics -XDshould-stop.at=FLOW PrimitivePatternsSwitchConstants.java + * @compile/fail/ref=PrimitivePatternsSwitchConstants.out -XDrawDiagnostics -XDshould-stop.at=FLOW -XDexhaustivityMaxBaseChecks=0 PrimitivePatternsSwitchConstants.java */ public class PrimitivePatternsSwitchConstants { void testConstExpressions() { diff --git a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java index dacd48f441c..3eab2fc83d0 100644 --- a/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java +++ b/test/langtools/tools/javac/patterns/PrimitivePatternsSwitchErrors.java @@ -3,7 +3,7 @@ * @bug 8304487 8325653 8332463 * @summary Compiler Implementation for Primitive types in patterns, instanceof, and switch (Preview) * @enablePreview - * @compile/fail/ref=PrimitivePatternsSwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW PrimitivePatternsSwitchErrors.java + * @compile/fail/ref=PrimitivePatternsSwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW -XDexhaustivityMaxBaseChecks=0 PrimitivePatternsSwitchErrors.java */ public class PrimitivePatternsSwitchErrors { record R_int(int x) {} diff --git a/test/langtools/tools/javac/patterns/SwitchErrors.java b/test/langtools/tools/javac/patterns/SwitchErrors.java index 607052be583..ebff13e7fa5 100644 --- a/test/langtools/tools/javac/patterns/SwitchErrors.java +++ b/test/langtools/tools/javac/patterns/SwitchErrors.java @@ -2,7 +2,7 @@ * @test /nodynamiccopyright/ * @bug 8262891 8269146 8269113 8348928 * @summary Verify errors related to pattern switches. - * @compile/fail/ref=SwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW SwitchErrors.java + * @compile/fail/ref=SwitchErrors.out -XDrawDiagnostics -XDshould-stop.at=FLOW -XDexhaustivityMaxBaseChecks=0 SwitchErrors.java */ public class SwitchErrors { diff --git a/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java b/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java index bdafef7e39a..9a378ce346a 100644 --- a/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java +++ b/test/langtools/tools/javac/platform/NonExportedPermittedTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,9 +26,9 @@ * @bug 8318913 * @summary Verify no error is when compiling a class whose permitted types are not exported * @modules jdk.compiler - * @compile/fail/ref=NonExportedPermittedTypes.out -XDrawDiagnostics NonExportedPermittedTypes.java - * @compile/fail/ref=NonExportedPermittedTypes.out --release 21 -XDrawDiagnostics NonExportedPermittedTypes.java - * @compile/fail/ref=NonExportedPermittedTypes.out --release ${jdk.version} -XDrawDiagnostics NonExportedPermittedTypes.java + * @compile/fail/ref=NonExportedPermittedTypes.out -XDrawDiagnostics -XDexhaustivityMaxBaseChecks=0 NonExportedPermittedTypes.java + * @compile/fail/ref=NonExportedPermittedTypes.out --release 21 -XDrawDiagnostics -XDexhaustivityMaxBaseChecks=0 NonExportedPermittedTypes.java + * @compile/fail/ref=NonExportedPermittedTypes.out --release ${jdk.version} -XDrawDiagnostics -XDexhaustivityMaxBaseChecks=0 NonExportedPermittedTypes.java */ diff --git a/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java b/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java index 802e66570ef..05a73c0ba7a 100644 --- a/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java +++ b/test/langtools/tools/javac/switchexpr/ExpressionSwitchNotExhaustive.java @@ -2,7 +2,7 @@ * @test /nodynamiccopyright/ * @bug 8206986 * @summary Verify behavior of not exhaustive switch expressions. - * @compile/fail/ref=ExpressionSwitchNotExhaustive.out -XDrawDiagnostics ExpressionSwitchNotExhaustive.java + * @compile/fail/ref=ExpressionSwitchNotExhaustive.out -XDrawDiagnostics -XDexhaustivityMaxBaseChecks=0 ExpressionSwitchNotExhaustive.java */ public class ExpressionSwitchNotExhaustive {