[java-highlighting] JEP 432: A 'case null, default' label dominates all other switch labels

A 'default' label dominates a case label with a case pattern, and it also dominates a case label with a 'null' case constant.

IDEA-309549

GitOrigin-RevId: 6877992c530e41a1200ea7c20d6405da4be0324b
This commit is contained in:
Andrey Cherkasov
2022-12-29 03:15:03 +04:00
committed by intellij-monorepo-bot
parent 5bc370c995
commit b56985873a
12 changed files with 223 additions and 37 deletions

View File

@@ -21,6 +21,7 @@ import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.containers.SmartHashSet;
import com.siyeh.ig.fixes.MakeDefaultLastCaseFix;
import com.siyeh.ig.psiutils.ControlFlowUtils;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.SwitchUtils;
@@ -501,7 +502,7 @@ public class SwitchBlockHighlightingModel {
if (body == null) return;
MultiMap<Object, PsiElement> elementsToCheckDuplicates = new MultiMap<>();
List<List<PsiSwitchLabelStatementBase>> elementsToCheckFallThroughLegality = new SmartList<>();
List<PsiCaseLabelElement> elementsToCheckDominance = new ArrayList<>();
List<PsiElement> elementsToCheckDominance = new ArrayList<>();
List<PsiCaseLabelElement> elementsToCheckCompleteness = new ArrayList<>();
int switchBlockGroupCounter = 0;
for (PsiStatement st : body.getStatements()) {
@@ -511,7 +512,9 @@ public class SwitchBlockHighlightingModel {
switchBlockGroupCounter++;
}
if (labelStatement.isDefaultCase()) {
elementsToCheckDuplicates.putValue(myDefaultValue, ObjectUtils.notNull(labelStatement.getFirstChild(), labelStatement));
PsiElement defaultKeyword = Objects.requireNonNull(labelStatement.getFirstChild());
elementsToCheckDuplicates.putValue(myDefaultValue, defaultKeyword);
elementsToCheckDominance.add(defaultKeyword);
continue;
}
PsiCaseLabelElementList labelElementList = labelStatement.getCaseLabelElementList();
@@ -667,36 +670,56 @@ public class SwitchBlockHighlightingModel {
}
@NotNull
private Map<PsiCaseLabelElement, PsiCaseLabelElement> findDominatedLabels(@NotNull List<? extends PsiCaseLabelElement> switchLabels) {
Map<PsiCaseLabelElement, PsiCaseLabelElement> result = new HashMap<>();
private Map<PsiCaseLabelElement, PsiElement> findDominatedLabels(@NotNull List<? extends PsiElement> switchLabels) {
Map<PsiCaseLabelElement, PsiElement> result = new HashMap<>();
for (int i = 0; i < switchLabels.size() - 1; i++) {
PsiCaseLabelElement current = switchLabels.get(i);
PsiElement current = switchLabels.get(i);
if (result.containsKey(current)) continue;
for (int j = i + 1; j < switchLabels.size(); j++) {
PsiCaseLabelElement next = switchLabels.get(j);
if (isConstantLabelElement(next)) {
PsiExpression constExpr = ObjectUtils.tryCast(next, PsiExpression.class);
assert constExpr != null;
if ((PsiUtil.getLanguageLevel(constExpr).isAtLeast(LanguageLevel.JDK_18_PREVIEW) ||
JavaPsiPatternUtil.isTotalForType(current, mySelectorType)) &&
JavaPsiPatternUtil.dominates(current, constExpr.getType())) {
result.put(next, current);
PsiElement next = switchLabels.get(j);
if (!(next instanceof PsiCaseLabelElement nextElement)) continue;
if (PsiUtil.getLanguageLevel(current).isAtLeast(LanguageLevel.JDK_20_PREVIEW) &&
!JavaPsiPatternUtil.isTotalForType(nextElement, mySelectorType) &&
((!(next instanceof PsiExpression expression) || ExpressionUtils.isNullLiteral(expression)) &&
current instanceof PsiKeyword &&
PsiKeyword.DEFAULT.equals(current.getText()) || isInCaseNullDefaultLabel(current))) {
// JEP 432
// A 'default' label dominates a case label with a case pattern,
// and it also dominates a case label with a null case constant.
// A 'case null, default' label dominates all other switch labels.
result.put(nextElement, current);
}
else if (current instanceof PsiCaseLabelElement currentElement) {
if (isConstantLabelElement(nextElement)) {
PsiExpression constExpr = ObjectUtils.tryCast(nextElement, PsiExpression.class);
assert constExpr != null;
if ((PsiUtil.getLanguageLevel(constExpr).isAtLeast(LanguageLevel.JDK_18_PREVIEW) ||
JavaPsiPatternUtil.isTotalForType(currentElement, mySelectorType)) &&
JavaPsiPatternUtil.dominates(currentElement, constExpr.getType())) {
result.put(nextElement, current);
}
}
else if (isNullType(nextElement) && JavaPsiPatternUtil.isTotalForType(currentElement, mySelectorType)
&& PsiUtil.getLanguageLevel(nextElement).isLessThan(LanguageLevel.JDK_19_PREVIEW)) {
result.put(nextElement, current);
}
else if (JavaPsiPatternUtil.dominates(currentElement, nextElement)) {
result.put(nextElement, current);
}
continue;
}
if (isNullType(next) && JavaPsiPatternUtil.isTotalForType(current, mySelectorType)
&& PsiUtil.getLanguageLevel(next).isLessThan(LanguageLevel.JDK_19_PREVIEW)) {
result.put(next, current);
continue;
}
if (JavaPsiPatternUtil.dominates(current, next)) {
result.put(next, current);
}
}
}
return result;
}
private static boolean isInCaseNullDefaultLabel(@NotNull PsiElement element) {
PsiCaseLabelElementList list = ObjectUtils.tryCast(element.getParent(), PsiCaseLabelElementList.class);
if (list == null || list.getElementCount() != 2) return false;
PsiCaseLabelElement[] elements = list.getElements();
return elements[0] instanceof PsiExpression expression && ExpressionUtils.isNullLiteral(expression) &&
elements[1] instanceof PsiDefaultCaseLabelElement;
}
@Override
HighlightInfo.@NotNull Builder createDuplicateInfo(@Nullable Object duplicateKey, @NotNull PsiElement duplicateElement) {
String description;
@@ -832,18 +855,29 @@ public class SwitchBlockHighlightingModel {
* @see JavaPsiPatternUtil#isTotalForType(PsiCaseLabelElement, PsiType)
* @see JavaPsiPatternUtil#dominates(PsiCaseLabelElement, PsiCaseLabelElement)
*/
private void checkDominance(@NotNull List<? extends PsiCaseLabelElement> switchLabels, @NotNull HighlightInfoHolder results) {
Map<PsiCaseLabelElement, PsiCaseLabelElement> dominatedLabels = findDominatedLabels(switchLabels);
private void checkDominance(@NotNull List<? extends PsiElement> switchLabels, @NotNull HighlightInfoHolder results) {
Map<PsiCaseLabelElement, PsiElement> dominatedLabels = findDominatedLabels(switchLabels);
dominatedLabels.forEach((overWhom, who) -> {
HighlightInfo.Builder info = createError(overWhom, JavaErrorBundle.message("switch.dominance.of.preceding.label", who.getText()));
PsiPattern overWhomPattern = ObjectUtils.tryCast(overWhom, PsiPattern.class);
PsiPattern whoPattern = ObjectUtils.tryCast(who, PsiPattern.class);
if (whoPattern == null || !JavaPsiPatternUtil.dominates(overWhomPattern, whoPattern)) {
IntentionAction action = getFixFactory().createMoveSwitchBranchUpFix(who, overWhom);
if (PsiUtil.getLanguageLevel(who).isAtLeast(LanguageLevel.JDK_20_PREVIEW) &&
who instanceof PsiKeyword && PsiKeyword.DEFAULT.equals(who.getText()) ||
isInCaseNullDefaultLabel(who)) {
PsiSwitchLabelStatementBase labelStatementBase = PsiTreeUtil.getParentOfType(who, PsiSwitchLabelStatementBase.class);
if (labelStatementBase != null) {
IntentionAction action = new MakeDefaultLastCaseFix(labelStatementBase);
info.registerFix(action, null, null, null, null);
}
}
else if (who instanceof PsiCaseLabelElement whoElement) {
PsiPattern overWhomPattern = ObjectUtils.tryCast(overWhom, PsiPattern.class);
PsiPattern whoPattern = ObjectUtils.tryCast(who, PsiPattern.class);
if (whoPattern == null || !JavaPsiPatternUtil.dominates(overWhomPattern, whoPattern)) {
IntentionAction action = getFixFactory().createMoveSwitchBranchUpFix(whoElement, overWhom);
info.registerFix(action, null, null, null, null);
}
IntentionAction action = getFixFactory().createDeleteSwitchLabelFix(overWhom);
info.registerFix(action, null, null, null, null);
}
IntentionAction action = getFixFactory().createDeleteSwitchLabelFix(overWhom);
info.registerFix(action, null, null, null, null);
results.add(info.create());
});
}
@@ -916,10 +950,23 @@ public class SwitchBlockHighlightingModel {
elements.add(labelElement);
}
else if (labelElement instanceof PsiExpression) {
if (isNullType(labelElement) || isConstantLabelElement(labelElement)) {
boolean isNullType = isNullType(labelElement);
if (isNullType &&
PsiUtil.getLanguageLevel(labelElement).isAtLeast(LanguageLevel.JDK_20_PREVIEW) &&
isInCaseNullDefaultLabel(labelElement)) {
// JEP 432
// A 'case null, default' label dominates all other switch labels.
//
// In this case, only the 'default' case will be added to the elements checked for dominance
return;
}
if (isNullType || isConstantLabelElement(labelElement)) {
elements.add(labelElement);
}
}
else if (labelElement instanceof PsiDefaultCaseLabelElement) {
elements.add(labelElement);
}
}
private void registerDeleteFixForDefaultElement(@NotNull HighlightInfo.Builder info, PsiElement defaultElement, @NotNull PsiElement duplicateElement) {
@@ -1141,15 +1188,13 @@ public class SwitchBlockHighlightingModel {
result.addAll(entry.getValue());
}
PatternsInSwitchBlockHighlightingModel patternInSwitchModel = ObjectUtils.tryCast(switchModel, PatternsInSwitchBlockHighlightingModel.class);
PatternsInSwitchBlockHighlightingModel patternInSwitchModel =
ObjectUtils.tryCast(switchModel, PatternsInSwitchBlockHighlightingModel.class);
if (patternInSwitchModel == null) return result;
List<PsiCaseLabelElement> dominanceCheckingCandidates = new SmartList<>();
labelElements.forEach(label -> PatternsInSwitchBlockHighlightingModel.fillElementsToCheckDominance(dominanceCheckingCandidates, label));
if (dominanceCheckingCandidates.isEmpty()) return result;
Set<PsiCaseLabelElement> dominatedPatterns = StreamEx.ofKeys(
patternInSwitchModel.findDominatedLabels(dominanceCheckingCandidates), value -> value instanceof PsiPattern).toSet();
result.addAll(dominatedPatterns);
return result;
return StreamEx.ofKeys(patternInSwitchModel.findDominatedLabels(dominanceCheckingCandidates), value -> value instanceof PsiPattern)
.into(result);
}
}

View File

@@ -0,0 +1,40 @@
class X {
void testDominance1(Object obj) {
switch (obj) {
default -> System.out.println("default");
case <error descr="Label is dominated by a preceding case label 'default'">Integer i</error> -> System.out.println("Integer");
case <error descr="Label is dominated by a preceding case label 'default'">String s when s.isEmpty()</error> -> System.out.println("empty String");
case <error descr="Label is dominated by a preceding case label 'default'">null</error> -> System.out.println("null");
}
}
void testDominance2(Object obj) {
switch (obj) {
case null, default -> System.out.println("null or default");
case <error descr="Label is dominated by a preceding case label 'default'">Integer i</error> -> System.out.println("Integer");
case <error descr="Label is dominated by a preceding case label 'default'">String s when s.isEmpty()</error> -> System.out.println("empty String");
}
}
void testDominance3(String s) {
switch (s) {
default -> System.out.println("default");
case "blah blah blah" -> System.out.println("blah blah blah");
case <error descr="Label is dominated by a preceding case label 'default'">null</error> -> System.out.println("null");
}
}
void testDominance4(String s) {
switch (s) {
case null, default -> System.out.println("null, default");
case <error descr="Label is dominated by a preceding case label 'default'">"blah blah blah"</error> -> System.out.println("blah blah blah");
}
}
void test(String s) {
switch (s) {
case null, <error descr="'switch' has both a total pattern and a default label">default</error> -> System.out.println("null, default");
case <error descr="'switch' has both a total pattern and a default label">String str</error> -> System.out.println("String");
}
}
}

View File

@@ -0,0 +1,11 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(Object obj) {
switch (obj) {
case Integer i -> System.out.println("Integer");
case String s when s.isEmpty() -> System.out.println("empty String");
case null -> System.out.println("null");
default -> System.out.println("default");
}
}
}

View File

@@ -0,0 +1,10 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(Object obj) {
switch (obj) {
case Integer i -> System.out.println("Integer");
case String s when s.isEmpty() -> System.out.println("empty String");
case null, default -> System.out.println("null or default");
}
}
}

View File

@@ -0,0 +1,10 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(String s) {
switch (s) {
case "blah blah blah" -> System.out.println("blah blah blah");
case null -> System.out.println("null");
default -> System.out.println("default");
}
}
}

View File

@@ -0,0 +1,9 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(String s) {
switch (s) {
case "blah blah blah" -> System.out.println("blah blah blah");
case null, default -> System.out.println("null, default");
}
}
}

View File

@@ -0,0 +1,11 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(Object obj) {
switch (obj) {
default -> System.out.println("default");
case Integer i<caret> -> System.out.println("Integer");
case String s when s.isEmpty() -> System.out.println("empty String");
case null -> System.out.println("null");
}
}
}

View File

@@ -0,0 +1,10 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(Object obj) {
switch (obj) {
case null, default -> System.out.println("null or default");
case Integer i -> System.out.println("Integer");
case String s when s.isEmpty()<caret> -> System.out.println("empty String");
}
}
}

View File

@@ -0,0 +1,10 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(String s) {
switch (s) {
default -> System.out.println("default");
case "blah blah blah" -> System.out.println("blah blah blah");
case null<caret> -> System.out.println("null");
}
}
}

View File

@@ -0,0 +1,9 @@
// "Make 'default' the last case" "true-preview"
class X {
void test(String s) {
switch (s) {
case null, default -> System.out.println("null, default");
case "blah blah blah"<caret> -> System.out.println("blah blah blah");
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.pom.java.LanguageLevel;
public class MakeDefaultLastCaseFixTest extends LightQuickFixParameterizedTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/makeDefaultLastCase";
}
@Override
protected LanguageLevel getLanguageLevel() {
return LanguageLevel.JDK_20_PREVIEW;
}
}

View File

@@ -37,6 +37,10 @@ public class LightPatternsForSwitchHighlightingTest extends LightJavaCodeInsight
IdeaTestUtil.withLevel(getModule(), LanguageLevel.JDK_19_PREVIEW, this::doTest);
}
public void testPatternsInSwitchIn20Java() {
IdeaTestUtil.withLevel(getModule(), LanguageLevel.JDK_20_PREVIEW, this::doTest);
}
public void testMismatchedDeconstructionIn19Java() {
IdeaTestUtil.withLevel(getModule(), LanguageLevel.JDK_19_PREVIEW, this::doTest);
}