diff --git a/plugins/kotlin/highlighting/highlighting-k2/test/org/jetbrains/kotlin/idea/k2/highlighting/K2HighlightExitPointsTestGenerated.java b/plugins/kotlin/highlighting/highlighting-k2/test/org/jetbrains/kotlin/idea/k2/highlighting/K2HighlightExitPointsTestGenerated.java index 4bf1b603f640..83bbb8e638e6 100644 --- a/plugins/kotlin/highlighting/highlighting-k2/test/org/jetbrains/kotlin/idea/k2/highlighting/K2HighlightExitPointsTestGenerated.java +++ b/plugins/kotlin/highlighting/highlighting-k2/test/org/jetbrains/kotlin/idea/k2/highlighting/K2HighlightExitPointsTestGenerated.java @@ -33,6 +33,36 @@ public class K2HighlightExitPointsTestGenerated extends AbstractK2HighlightExitP runTest("../../idea/tests/testData/exitPoints/anonymousFunction2.kt"); } + @TestMetadata("forLoopBreak.kt") + public void testForLoopBreak() throws Exception { + runTest("../../idea/tests/testData/exitPoints/forLoopBreak.kt"); + } + + @TestMetadata("forLoopNestedBreak.kt") + public void testForLoopNestedBreak() throws Exception { + runTest("../../idea/tests/testData/exitPoints/forLoopNestedBreak.kt"); + } + + @TestMetadata("forLoopNestedBreak2.kt") + public void testForLoopNestedBreak2() throws Exception { + runTest("../../idea/tests/testData/exitPoints/forLoopNestedBreak2.kt"); + } + + @TestMetadata("forLoopNestedLabeledBreak.kt") + public void testForLoopNestedLabeledBreak() throws Exception { + runTest("../../idea/tests/testData/exitPoints/forLoopNestedLabeledBreak.kt"); + } + + @TestMetadata("forLoopNestedLabeledBreakOnLoop.kt") + public void testForLoopNestedLabeledBreakOnLoop() throws Exception { + runTest("../../idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop.kt"); + } + + @TestMetadata("forLoopNestedLabeledBreakOnLoop2.kt") + public void testForLoopNestedLabeledBreakOnLoop2() throws Exception { + runTest("../../idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop2.kt"); + } + @TestMetadata("funKeyword.kt") public void testFunKeyword() throws Exception { runTest("../../idea/tests/testData/exitPoints/funKeyword.kt"); diff --git a/plugins/kotlin/highlighting/highlighting-shared/src/org/jetbrains/kotlin/idea/base/highlighting/AbstractKotlinHighlightExitPointsHandlerFactory.kt b/plugins/kotlin/highlighting/highlighting-shared/src/org/jetbrains/kotlin/idea/base/highlighting/AbstractKotlinHighlightExitPointsHandlerFactory.kt index aae19678aa4d..6af9d5cdeb4b 100644 --- a/plugins/kotlin/highlighting/highlighting-shared/src/org/jetbrains/kotlin/idea/base/highlighting/AbstractKotlinHighlightExitPointsHandlerFactory.kt +++ b/plugins/kotlin/highlighting/highlighting-shared/src/org/jetbrains/kotlin/idea/base/highlighting/AbstractKotlinHighlightExitPointsHandlerFactory.kt @@ -19,6 +19,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.resolve.inline.InlineUtil import org.jetbrains.kotlin.utils.addIfNotNull +import java.util.* abstract class AbstractKotlinHighlightExitPointsHandlerFactory : HighlightUsagesHandlerFactoryBase() { private fun getOnReturnOrThrowUsageHandler(editor: Editor, file: PsiFile, target: PsiElement): HighlightUsagesHandlerBase<*>? { @@ -35,6 +36,15 @@ abstract class AbstractKotlinHighlightExitPointsHandlerFactory : HighlightUsages return OnExitUsagesHandler(editor, file, expression, false) } + private fun getOnBreakOrContinueUsageHandler(editor: Editor, file: PsiFile, target: PsiElement): HighlightUsagesHandlerBase<*>? { + val expression = when (val parent = target.parent) { + is KtBreakExpression, is KtContinueExpression -> parent + is KtLoopExpression -> parent + else -> null + } as? KtExpression ?: return null + return OnLoopUsagesHandler(editor, file, expression, false) + } + private fun getOnLambdaCallUsageHandler(editor: Editor, file: PsiFile, target: PsiElement): HighlightUsagesHandlerBase<*>? { if (target !is LeafPsiElement || target.elementType !is KtToken // do not trigger loading of KtTokens in Java @@ -57,6 +67,7 @@ abstract class AbstractKotlinHighlightExitPointsHandlerFactory : HighlightUsages override fun createHighlightUsagesHandler(editor: Editor, file: PsiFile, target: PsiElement): HighlightUsagesHandlerBase<*>? { return getOnReturnOrThrowUsageHandler(editor, file, target) + ?: getOnBreakOrContinueUsageHandler(editor, file, target) ?: getOnLambdaCallUsageHandler(editor, file, target) } @@ -91,7 +102,7 @@ abstract class AbstractKotlinHighlightExitPointsHandlerFactory : HighlightUsages private inner class OnExitUsagesHandler(editor: Editor, file: PsiFile, val target: KtExpression, val highlightReferences: Boolean) : HighlightUsagesHandlerBase(editor, file) { - override fun getTargets() = listOf(target) + override fun getTargets(): List = listOf(target) override fun selectTargets(targets: List, selectionConsumer: Consumer>) { selectionConsumer.consume(targets) @@ -290,6 +301,89 @@ abstract class AbstractKotlinHighlightExitPointsHandlerFactory : HighlightUsages override fun highlightReferences(): Boolean = highlightReferences } + private inner class OnLoopUsagesHandler(editor: Editor, file: PsiFile, val target: KtExpression, val highlightReferences: Boolean) : + HighlightUsagesHandlerBase(editor, file) { + override fun getTargets(): List = listOf(target) + + override fun selectTargets(targets: List, selectionConsumer: Consumer>) { + selectionConsumer.consume(targets) + } + + override fun computeUsages(targets: MutableList) { + val labelName = when (target) { + is KtExpressionWithLabel -> target.getLabelName() + is KtLoopExpression -> (target.parent as? KtLabeledExpression)?.getLabelName() + else -> null + } + val relevantLoop: KtLoopExpression = when (target) { + is KtLoopExpression -> target + else -> { + var element: PsiElement? = target + var targetLoop: KtLoopExpression? = null + while (element != null) { + val parent = element.parent + if (element is KtLoopExpression && (labelName == null || (parent as? KtLabeledExpression)?.getLabelName() == labelName)) { + targetLoop = element + break + } + element = parent + } + targetLoop + } + } ?: return + + when(relevantLoop) { + is KtForExpression -> addOccurrence(relevantLoop.forKeyword) + is KtDoWhileExpression -> relevantLoop.node.findChildByType(KtTokens.DO_KEYWORD)?.psi?.let(::addOccurrence) + is KtWhileExpression -> relevantLoop.node.findChildByType(KtTokens.WHILE_KEYWORD)?.psi?.let(::addOccurrence) + } + + + + relevantLoop.accept(object : KtVisitorVoid() { + var nestedLoopExpressions = Stack() + + override fun visitKtElement(element: KtElement) { + ProgressIndicatorProvider.checkCanceled() + element.acceptChildren(this) + } + + override fun visitExpression(expression: KtExpression) { + val nestedLoopFound = if (expression != relevantLoop && expression is KtLoopExpression) { + val loopLabelName = (expression.parent as? KtLabeledExpression)?.getLabelName() + // no reasons to step into another loop with the same label name or no label name + if (labelName == null || labelName == loopLabelName) return + + nestedLoopExpressions.push(expression) + true + } else { + false + } + + if (expression is KtBreakExpression || expression is KtContinueExpression) { + val expressionLabelName = (expression as? KtExpressionWithLabel)?.getLabelName() + if (nestedLoopExpressions.isEmpty()) { + if (expressionLabelName == null || expressionLabelName == labelName) { + addOccurrence(expression) + } + } else if (expressionLabelName == labelName) { + addOccurrence(expression) + } + } + + try { + super.visitExpression(expression) + } finally { + if (nestedLoopFound) { + nestedLoopExpressions.pop() + } + } + } + }) + } + + } + private fun MutableSet.addIfNotNullAndNotBlock(element: PsiElement?) { addIfNotNull(element.takeUnless { it is KtBlockExpression }) } diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/highlighter/HighlightExitPointsTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/highlighter/HighlightExitPointsTestGenerated.java index 8293f7a73901..838d88c6d11e 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/highlighter/HighlightExitPointsTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/highlighter/HighlightExitPointsTestGenerated.java @@ -33,6 +33,36 @@ public class HighlightExitPointsTestGenerated extends AbstractHighlightExitPoint runTest("testData/exitPoints/anonymousFunction2.kt"); } + @TestMetadata("forLoopBreak.kt") + public void testForLoopBreak() throws Exception { + runTest("testData/exitPoints/forLoopBreak.kt"); + } + + @TestMetadata("forLoopNestedBreak.kt") + public void testForLoopNestedBreak() throws Exception { + runTest("testData/exitPoints/forLoopNestedBreak.kt"); + } + + @TestMetadata("forLoopNestedBreak2.kt") + public void testForLoopNestedBreak2() throws Exception { + runTest("testData/exitPoints/forLoopNestedBreak2.kt"); + } + + @TestMetadata("forLoopNestedLabeledBreak.kt") + public void testForLoopNestedLabeledBreak() throws Exception { + runTest("testData/exitPoints/forLoopNestedLabeledBreak.kt"); + } + + @TestMetadata("forLoopNestedLabeledBreakOnLoop.kt") + public void testForLoopNestedLabeledBreakOnLoop() throws Exception { + runTest("testData/exitPoints/forLoopNestedLabeledBreakOnLoop.kt"); + } + + @TestMetadata("forLoopNestedLabeledBreakOnLoop2.kt") + public void testForLoopNestedLabeledBreakOnLoop2() throws Exception { + runTest("testData/exitPoints/forLoopNestedLabeledBreakOnLoop2.kt"); + } + @TestMetadata("funKeyword.kt") public void testFunKeyword() throws Exception { runTest("testData/exitPoints/funKeyword.kt"); diff --git a/plugins/kotlin/idea/tests/testData/exitPoints/forLoopBreak.kt b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopBreak.kt new file mode 100644 index 000000000000..c6e241c2b5aa --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopBreak.kt @@ -0,0 +1,5 @@ +fun some() { + for (i in 0..5) { + break~ + } +} diff --git a/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedBreak.kt b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedBreak.kt new file mode 100644 index 000000000000..c53a7cbd7c15 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedBreak.kt @@ -0,0 +1,7 @@ +fun some() { + for (j in 0..5) { + for (i in 0..5) { + break~ + } + } +} diff --git a/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedBreak2.kt b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedBreak2.kt new file mode 100644 index 000000000000..6535c305fabd --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedBreak2.kt @@ -0,0 +1,12 @@ +fun some() { + for~ (j in 0..5) { + if (j == 1) break + for (i in 0..5) { + break + } + loop@ for (i in 0..5) { + break + } + if (j == 2) continue + } +} diff --git a/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreak.kt b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreak.kt new file mode 100644 index 000000000000..9f5496ec5fa2 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreak.kt @@ -0,0 +1,13 @@ +fun some(list: List) { + a@ for (j in 0..5) { + for (i in 0..5) { + break@a + } + if (j == 0) continue + if (j == 2) break + b@ for (i in 0..5) { + break@b + brea~k@a + } + } +} diff --git a/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop.kt b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop.kt new file mode 100644 index 000000000000..fe8d06b9c3aa --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop.kt @@ -0,0 +1,12 @@ +fun some(list: List) { + loop@ for~ (s in l) { + if (s == "a") break@loop + if (s == "b") continue + + for (s1 in l) { + if (s1 == "a") break + if (s1 == "b") continue + if (s1 == "c") break@loop + } + } +} diff --git a/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop2.kt b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop2.kt new file mode 100644 index 000000000000..3e9e1551bc3a --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/exitPoints/forLoopNestedLabeledBreakOnLoop2.kt @@ -0,0 +1,34 @@ +fun some(list: List) { + loop@ for~ (s in l) { + if (s == "a") break@loop + if (s == "b") continue + + loop@ for (s1 in l) { + if (s1 == "a") break + if (s1 == "b") continue + if (s1 == "c") break@loop + } + + loop2@ for (s1 in l) { + if (s1 == "a") break + if (s1 == "b") continue + if (s1 == "c") break@loop + if (s1 == "d") break + } + + loop1@ for (s1 in l) { + if (s1 == "a") break + if (s1 == "b") continue + loop2@ for (s1 in l) { + if (s1 == "a") break + if (s1 == "b") continue + if (s1 == "c") break@loop + } + if (s1 == "c") break + } + + if (s == "c") continue + if (s == "d") break + if (s == "e") break@loop + } +}