[java][resolve] IDEA-271676 Pattern matching for switch: fix resolve

Fix the resolution algorithm for both switch statements and switch expressions. The main obstacle is that the handler of a case expression is a child node of the labeled case rule and the handler of a case statement is the right sibling of the labeled case rule.

The fact that a case handler is the right sibling of a labeled case rule in switch statements complicates the resolving, because the scope of a pattern variable is bound only to the immediate case handler and cannot be accessible from different case handler, which might be invoked when there is no `break` statements between case rules. In order to restrict the scope of a pattern variable in a switch statement the following check is added: check if the analyzed PsiSwitchLabeledStatement is not followed by any other case rules and if so try to resolve an element using its pattern variables.

The scope of pattern variables for PsiSwitchLabeledRuleStatements is restricted only to their case handlers, so the resolve works there only if the lastParent is not null, which, according to the contract, contains the case's handler.

Signed-off-by: Nikita Eshkeev <nikita.eshkeev@jetbrains.com>

GitOrigin-RevId: 084b8b679b4070a67a996c3b50992622025d96ee
This commit is contained in:
Nikita Eshkeev
2021-06-23 00:33:25 +00:00
committed by intellij-monorepo-bot
parent 9ca695b1fe
commit 0e5e22a101
13 changed files with 124 additions and 61 deletions
@@ -638,7 +638,7 @@ public final class HighlightUtil {
declarationScope instanceof PsiForeachStatement ||
declarationScope instanceof PsiLambdaExpression)) {
PsiElement scope =
PsiTreeUtil.getParentOfType(variable, PsiFile.class, PsiMethod.class, PsiClassInitializer.class, PsiResourceList.class, PsiSwitchLabeledRuleStatement.class);
PsiTreeUtil.getParentOfType(variable, PsiFile.class, PsiMethod.class, PsiClassInitializer.class, PsiResourceList.class);
VariablesNotProcessor proc = new VariablesNotProcessor(variable, false) {
@Override
protected boolean check(final PsiVariable var, final ResolveState state) {
@@ -1048,10 +1048,6 @@ public final class PsiUtil extends PsiUtilCore {
return getLanguageLevel(element).isAtLeast(LanguageLevel.JDK_16);
}
public static boolean isLanguageLevel17OrHigher(@NotNull PsiElement element) {
return getLanguageLevel(element).isAtLeast(LanguageLevel.JDK_17);
}
@NotNull
public static LanguageLevel getLanguageLevel(@NotNull PsiElement element) {
if (element instanceof PsiDirectory) {
@@ -2,11 +2,9 @@
package com.intellij.psi.impl.source.tree.java;
import com.intellij.lang.ASTNode;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiCaseLabelElement;
import com.intellij.psi.PsiCaseLabelElementList;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.*;
import com.intellij.psi.scope.PsiScopeProcessor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -85,4 +83,18 @@ public class PsiCaseLabelElementListImpl extends CompositePsiElement implements
public String toString() {
return "PsiCaseLabelElementList";
}
@Override
public boolean processDeclarations(@NotNull PsiScopeProcessor processor,
@NotNull ResolveState state,
PsiElement lastParent,
@NotNull PsiElement place) {
// Do not resolve elements from the list of elements of the case rule
if (lastParent != null) return true;
for (PsiCaseLabelElement label : getElements()) {
boolean shouldKeepGoing = label.processDeclarations(processor, state, null, place);
if (!shouldKeepGoing) return false;
}
return true;
}
}
@@ -17,7 +17,6 @@ import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.scope.util.PsiScopesUtil;
import com.intellij.psi.tree.ChildRoleBase;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -268,18 +267,9 @@ public final class PsiCodeBlockImpl extends LazyParseablePsiElement implements P
child = ((PsiElement)this).getLastChild();
}
final boolean projectLevelAtLeast17 = PsiUtil.isLanguageLevel17OrHigher(place);
boolean shouldStop = false;
while (!shouldStop && child != null) {
while (child != null) {
if (child instanceof PsiSwitchLabelStatementBase) {
state = PatternResolveState.WHEN_NONE.putInto(state);
if (child instanceof PsiSwitchLabelStatement) {
// The scope of a pattern variable declaration which occurs in a case label of
// a switch labeled statement group, where there are no further switch labels that follow,
// includes the block statements of the statement group.
// See more https://openjdk.java.net/jeps/406#3--Scope-of-pattern-variable-declarations
shouldStop = projectLevelAtLeast17;
}
}
if (!child.processDeclarations(processor, state, null, place)) return false;
child = child.getPrevSibling();
@@ -65,14 +65,6 @@ public abstract class PsiSwitchLabelStatementBaseImpl extends CompositePsiElemen
}
}
final PsiCaseLabelElementList patternsInCaseLabel = getCaseLabelElementList();
if (patternsInCaseLabel == null) return true;
for (PsiCaseLabelElement label : patternsInCaseLabel.getElements()) {
boolean shouldKeepGoing = label.processDeclarations(processor, state, null, place);
if (!shouldKeepGoing) return false;
}
return true;
}
@@ -1,17 +1,16 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.psi.impl.source.tree.java;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiSwitchLabelStatement;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.ChildRole;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.JavaElementType;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.tree.ChildRoleBase;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
public class PsiSwitchLabelStatementImpl extends PsiSwitchLabelStatementBaseImpl implements PsiSwitchLabelStatement {
@@ -58,4 +57,34 @@ public class PsiSwitchLabelStatementImpl extends PsiSwitchLabelStatementBaseImpl
public String toString() {
return "PsiSwitchLabelStatement";
}
@Override
public boolean processDeclarations(@NotNull PsiScopeProcessor processor,
@NotNull ResolveState state,
PsiElement lastParent,
@NotNull PsiElement place) {
if (!super.processDeclarations(processor, state, lastParent, place)) return false;
// Do not resolve references that come from the list of elements in this case rule
if (lastParent instanceof PsiCaseLabelElementList) return true;
if (isNotImmediateSwitchLabel()) return true;
final PsiCaseLabelElementList patternsInCaseLabel = getCaseLabelElementList();
if (patternsInCaseLabel == null) return true;
return patternsInCaseLabel.processDeclarations(processor, state, null, place);
}
/**
* When the resolving is happening inside a {@link PsiCodeBlock} it traverses through all the case labels,
* which is not what is expected for pattern variables, because their scope is bound only to the nearest case handler.
* The method checks if this {@link PsiSwitchLabelStatement} is the nearest one to the case handler.
*
* @return true if the this {@link PsiSwitchLabelStatement} is followed by another {@link PsiSwitchLabelStatement}, false otherwise
*/
private boolean isNotImmediateSwitchLabel() {
final PsiElement rightNeighbour = PsiTreeUtil.skipWhitespacesForward(this);
return rightNeighbour instanceof PsiSwitchLabelStatement;
}
}
@@ -40,6 +40,15 @@ public class PsiSwitchLabeledRuleStatementImpl extends PsiSwitchLabelStatementBa
@NotNull ResolveState state,
PsiElement lastParent,
@NotNull PsiElement place) {
return lastParent == null || super.processDeclarations(processor, state, lastParent, place);
if (!super.processDeclarations(processor, state, lastParent, place)) return false;
// Do not resolve variables that don't come from the body of this {@link PsiSwitchLabeledRuleStatement}
if (lastParent == null) return true;
// Do not resolve references that come from the list of elements in this case rule
if (lastParent instanceof PsiCaseLabelElementList) return true;
final PsiCaseLabelElementList patternsInCaseLabel = getCaseLabelElementList();
if (patternsInCaseLabel == null) return true;
return patternsInCaseLabel.processDeclarations(processor, state, null, place);
}
}
@@ -3,14 +3,15 @@ package com.intellij.psi.impl.source.tree.java;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.*;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.impl.source.tree.ChildRole;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.JavaElementType;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.tree.ChildRoleBase;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
public class PsiSwitchStatementImpl extends PsiSwitchBlockImpl implements PsiSwitchStatement {
@@ -55,29 +56,6 @@ public class PsiSwitchStatementImpl extends PsiSwitchBlockImpl implements PsiSwi
}
}
@Override
public boolean processDeclarations(@NotNull PsiScopeProcessor processor,
@NotNull ResolveState state,
PsiElement lastParent,
@NotNull PsiElement place) {
if (lastParent == null) return true;
final PsiElement caseHandlerBlock;
if (PsiTreeUtil.isAncestor(lastParent, place, true)) {
caseHandlerBlock = lastParent;
}
else {
caseHandlerBlock = null;
}
if (caseHandlerBlock == null) return true;
final PsiElement prevCaseLabel = PsiTreeUtil.findSiblingBackward(caseHandlerBlock, JavaElementType.SWITCH_LABEL_STATEMENT, null);
if (!(prevCaseLabel instanceof PsiSwitchLabelStatement)) return true;
return prevCaseLabel.processDeclarations(processor, state, null, place);
}
@Override
public String toString() {
return "PsiSwitchStatement";
@@ -0,0 +1,12 @@
class Main {
static void m(Object obj, int x) {
switch (x) {
case 1:
if (!(obj instanceof String s)) break;
System.out.println(s);
case 2:
System.out.println(<error descr="Cannot resolve symbol 's'">s</error>);
}
}
}
@@ -0,0 +1,10 @@
class Main {
final int i = 2;
String test(Object obj) {
return switch (obj) {
case i<caret>, Integer i -> "hello";
default -> "nothing";
}
}
}
@@ -0,0 +1,11 @@
class Main {
final int i = 2;
void f(Object obj) {
switch (obj) {
case i<caret>, Integer i:
System.out.println(i);
}
}
}
}
@@ -36,6 +36,10 @@ public class LightPatternsForSwitchHighlightingTest extends LightJavaCodeInsight
doTest();
}
public void testPatternMatchingInSwitchWithIfPatternMatching() {
doTest();
}
private void doTest() {
myFixture.configureByFile(getTestName(false) + ".java");
myFixture.checkHighlighting();
@@ -1,4 +1,4 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.java.codeInsight.navigation;
import com.intellij.JavaTestUtil;
@@ -6,6 +6,7 @@ import com.intellij.codeInsight.TargetElementUtil;
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.testFramework.LightJavaCodeInsightTestCase;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -13,6 +14,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import static com.intellij.testFramework.assertions.Assertions.assertThat;
public class GotoDeclarationTest extends LightJavaCodeInsightTestCase {
@NotNull
@Override
@@ -118,4 +121,21 @@ public class GotoDeclarationTest extends LightJavaCodeInsightTestCase {
PsiElement element = GotoDeclarationAction.findTargetElement(getProject(), getEditor(), getEditor().getCaretModel().getOffset());
assertNotNull("Unexpected null", element);
}
public void testPatternMatchingGuardInSwitchExpression() {
doTestPatternMatchingGuard();
}
public void testPatternMatchingGuardInSwitchStatement() {
doTestPatternMatchingGuard();
}
private void doTestPatternMatchingGuard() {
String name = getTestName(false);
configureByFile("/codeInsight/gotoDeclaration/" + name + ".java");
final PsiJavaFile file = (PsiJavaFile)getFile();
final PsiField field = PsiTreeUtil.findChildOfType(file, PsiField.class);
final PsiElement element = GotoDeclarationAction.findTargetElement(getProject(), getEditor(), getEditor().getCaretModel().getOffset());
assertThat(element).isEqualTo(field);
}
}