diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/jetPsiUtil.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/jetPsiUtil.kt index 54b06946750..30db995ef39 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/jetPsiUtil.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/jetPsiUtil.kt @@ -536,6 +536,16 @@ inline fun flatMapDescendantsOfTypeVisitor( inline fun PsiElement.forEachDescendantsOfType(noinline block: (T) -> Unit) = accept(forEachDescendantOfTypeVisitor(block)) +inline fun PsiElement.anyDescendantOfType(noinline predicate: (T) -> Boolean): Boolean { + var result = false + accept(forEachDescendantOfTypeVisitor { + if (!result && predicate(it)) { + result = true + } + }) + return result +} + public fun PsiFile.getFqNameByDirectory(): FqName { val qualifiedNameByDirectory = getParent()?.getPackage()?.getQualifiedName() return qualifiedNameByDirectory?.let { FqName(it) } ?: FqName.ROOT diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java index 94251e29561..aa82d5bf88b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java @@ -340,5 +340,8 @@ public class QuickFixRegistrar { QuickFixes.factories.put(FUNCTION_EXPRESSION_WITH_NAME, RemoveNameFromFunctionExpressionFix.Companion); QuickFixes.factories.put(FUNCTION_EXPRESSION_WITH_NAME, RemoveNameFromFunctionExpressionFix.Companion.createWholeProjectFixFactory()); + + QuickFixes.factories.put(UNRESOLVED_REFERENCE, ReplaceObsoleteLabelSyntaxFix.Companion); + QuickFixes.factories.put(UNRESOLVED_REFERENCE, ReplaceObsoleteLabelSyntaxFix.Companion.createWholeProjectFixFactory()); } } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt new file mode 100644 index 00000000000..0373b25b56e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/ReplaceObsoleteLabelSyntaxFix.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.quickfix + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.quickfix.quickfixUtil.createIntentionFactory +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType +import org.jetbrains.kotlin.resolve.BindingContext + +public class ReplaceObsoleteLabelSyntaxFix(element: JetAnnotationEntry?) : JetIntentionAction(element) { + override fun getFamilyName(): String = "Update obsolete label syntax" + override fun getText(): String = "Replace with label ${element.getCalleeExpression()?.getText() ?: ""}@" + + override fun invoke(project: Project, editor: Editor?, file: JetFile) = replaceWithLabel(element) + + companion object : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val annotationEntry = diagnostic.getPsiElement().getNonStrictParentOfType() ?: return null + + if (!annotationEntry.looksLikeObsoleteLabel()) return null + + return ReplaceObsoleteLabelSyntaxFix(annotationEntry) + } + + public fun createWholeProjectFixFactory(): JetSingleIntentionActionFactory = createIntentionFactory factory@ { + diagnostic -> + + if (!(diagnostic.getPsiElement().getNonStrictParentOfType()?.looksLikeObsoleteLabelWithReferencesInCode() + ?: false)) return@factory null + + JetWholeProjectForEachElementOfTypeFix.createForMultiTask( + tasksFactory = { collectTasks(it) }, + tasksProcessor ={ it.forEach { ann -> replaceWithLabel(ann) } }, + modalTitle = "Replacing labels with obsolete syntax", + name = "Update obsolete label syntax in whole project", + familyName = "Update obsolete label syntax in whole project" + ) + } + + private fun collectTasks(expression: JetAnnotatedExpression) = + expression.getAnnotationEntries().filter { it.looksLikeObsoleteLabelWithReferencesInCode() } + + private fun JetAnnotationEntry.looksLikeObsoleteLabelWithReferencesInCode(): Boolean { + if (!looksLikeObsoleteLabel()) return false + + val baseExpression = (getParent() as? JetAnnotatedExpression)?.getBaseExpression() ?: return false + + val nameExpression = getCalleeExpression().getConstructorReferenceExpression() ?: return false + val labelName = nameExpression.getReferencedName() + + return baseExpression.anyDescendantOfType { + (it is JetBreakExpression || it is JetContinueExpression || it is JetReturnExpression) && + it.getLabelName() == labelName && + it.getTargetLabel()?.analyze()?.get(BindingContext.LABEL_TARGET, it.getTargetLabel()) == null + } && analyze().getDiagnostics().forElement(nameExpression).any { it.getFactory() == Errors.UNRESOLVED_REFERENCE } + } + + private fun JetAnnotationEntry.looksLikeObsoleteLabel() = + getAtSymbol() != null && + getParent() is JetAnnotatedExpression && + (getParent() as JetAnnotatedExpression).getAnnotationEntries().size() == 1 && + getValueArgumentList() == null && + getCalleeExpression()?.getConstructorReferenceExpression()?.getIdentifier() != null + + private fun replaceWithLabel(annotation: JetAnnotationEntry) { + val labelName = annotation.getCalleeExpression()?.getConstructorReferenceExpression()?.getReferencedName() ?: return + val annotatedExpression = annotation.getParent() as? JetAnnotatedExpression ?: return + val expression = annotatedExpression.getBaseExpression() ?: return + + if (annotatedExpression.getAnnotationEntries().size() != 1) return + + val baseExpressionStart = expression.getTextRange().getStartOffset() + + val textRangeToRetain = TextRange(annotation.getTextRange().getEndOffset(), baseExpressionStart) + val textToRetain = textRangeToRetain.substring(annotation.getContainingFile().getText()) + + val labeledExpression = JetPsiFactory(annotation).createExpressionByPattern("$labelName@$0$1", textToRetain, expression) + + annotatedExpression.replace(labeledExpression) + } + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt b/idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt new file mode 100644 index 00000000000..f422ab0f84e --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt @@ -0,0 +1,9 @@ +// "Replace with label label@" "true" + +fun run(block: () -> Unit) = block() + +fun foo() { + run @label { + return@label + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt.after b/idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt.after new file mode 100644 index 00000000000..d1bc777164e --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt.after @@ -0,0 +1,9 @@ +// "Replace with label label@" "true" + +fun run(block: () -> Unit) = block() + +fun foo() { + run label@ { + return@label + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt b/idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt new file mode 100644 index 00000000000..3c1e5b5579a --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt @@ -0,0 +1,7 @@ +// "Replace with label loop@" "true" + +fun foo() { + @loop for (i in 1..100) { + break@loop + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt.after b/idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt.after new file mode 100644 index 00000000000..bf62ccdc9e2 --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt.after @@ -0,0 +1,7 @@ +// "Replace with label loop@" "true" + +fun foo() { + loop@ for (i in 1..100) { + break@loop + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.after.data.Sample.kt b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.after.data.Sample.kt new file mode 100644 index 00000000000..b1236eb26d0 --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.after.data.Sample.kt @@ -0,0 +1,5 @@ +fun bar() { + l@ while (true) { + break@l + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.after.kt b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.after.kt new file mode 100644 index 00000000000..691951117b5 --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.after.kt @@ -0,0 +1,92 @@ +// "Update obsolete label syntax in whole project" "true" +// ERROR: Unresolved reference: @abc +// ERROR: Unresolved reference: @cde +// ERROR: Unresolved reference: @data +// ERROR: Unresolved reference: @labeled +// ERROR: Unresolved reference: @loop +// ERROR: Unresolved reference: @loop2 +// ERROR: Unresolved reference: @loop3 +// ERROR: Unresolved reference: @loop4 +// ERROR: Unresolved reference: @loop5 +// ERROR: Unresolved reference: abc +// ERROR: Unresolved reference: cde +// ERROR: Unresolved reference: labeled +// ERROR: Unresolved reference: loop +// ERROR: Unresolved reference: loop1 +// ERROR: Unresolved reference: loop2 +// ERROR: Unresolved reference: loop2 +// ERROR: Unresolved reference: loop3 +// ERROR: Unresolved reference: loop4 +// ERROR: Unresolved reference: loop5 +// ERROR: Unresolved reference: noreferences +// ERROR: Unresolved reference: notLabelAnnotation +// ERROR: Unresolved reference: notLoop +// ERROR: The label '@abc' does not denote a loop +// ERROR: The label '@cde' does not denote a loop +// ERROR: The label '@data' does not denote a loop +// ERROR: The label '@loop' does not denote a loop +// ERROR: The label '@loop2' does not denote a loop +// ERROR: The label '@loop3' does not denote a loop +// ERROR: The label '@loop4' does not denote a loop +// ERROR: The label '@loop5' does not denote a loop + +fun run(block: () -> Unit) = block() + +notLabelAnnotation class A { + fun foo() { + loop@ + for (i in 1..100) { + /* comment */ + continue@loop + } + + @noreferences for (i in 1..100) { + val x = 1 + } + + @loop1 for (i in 1..100) { + loop1@ for (j in 1..100) { + continue@loop1 + } + } + + loop2@ for (i in 1..100) { + loop2@ for (j in 1..100) { + break@loop2 + } + } + + @[loop3] for (i in 1..100) { + continue@loop3 + } + + run() labeled@ { + return@labeled + } + + @data for (i in 1..100) { + continue@data + } + + @ for (i in 1..100) { + break@ + } + + @loop4() for (i in 1..100) { + continue@loop4 + } + + @notLoop class Local + + @abc @cde for (i in 1..100) { + break@cde + break@abc + } + + /* 123 */ loop5@ /* 456 */ + + for (i in 1..100) { + break@loop5 + } /* 789 */ + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.Main.kt b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.Main.kt new file mode 100644 index 00000000000..36d074e292a --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.Main.kt @@ -0,0 +1,92 @@ +// "Update obsolete label syntax in whole project" "true" +// ERROR: Unresolved reference: @abc +// ERROR: Unresolved reference: @cde +// ERROR: Unresolved reference: @data +// ERROR: Unresolved reference: @labeled +// ERROR: Unresolved reference: @loop +// ERROR: Unresolved reference: @loop2 +// ERROR: Unresolved reference: @loop3 +// ERROR: Unresolved reference: @loop4 +// ERROR: Unresolved reference: @loop5 +// ERROR: Unresolved reference: abc +// ERROR: Unresolved reference: cde +// ERROR: Unresolved reference: labeled +// ERROR: Unresolved reference: loop +// ERROR: Unresolved reference: loop1 +// ERROR: Unresolved reference: loop2 +// ERROR: Unresolved reference: loop2 +// ERROR: Unresolved reference: loop3 +// ERROR: Unresolved reference: loop4 +// ERROR: Unresolved reference: loop5 +// ERROR: Unresolved reference: noreferences +// ERROR: Unresolved reference: notLabelAnnotation +// ERROR: Unresolved reference: notLoop +// ERROR: The label '@abc' does not denote a loop +// ERROR: The label '@cde' does not denote a loop +// ERROR: The label '@data' does not denote a loop +// ERROR: The label '@loop' does not denote a loop +// ERROR: The label '@loop2' does not denote a loop +// ERROR: The label '@loop3' does not denote a loop +// ERROR: The label '@loop4' does not denote a loop +// ERROR: The label '@loop5' does not denote a loop + +fun run(block: () -> Unit) = block() + +notLabelAnnotation class A { + fun foo() { + @loop + for (i in 1..100) { + /* comment */ + continue@loop + } + + @noreferences for (i in 1..100) { + val x = 1 + } + + @loop1 for (i in 1..100) { + loop1@ for (j in 1..100) { + continue@loop1 + } + } + + @loop2 for (i in 1..100) { + @loop2 for (j in 1..100) { + break@loop2 + } + } + + @[loop3] for (i in 1..100) { + continue@loop3 + } + + run() @labeled { + return@labeled + } + + @data for (i in 1..100) { + continue@data + } + + @ for (i in 1..100) { + break@ + } + + @loop4() for (i in 1..100) { + continue@loop4 + } + + @notLoop class Local + + @abc @cde for (i in 1..100) { + break@cde + break@abc + } + + /* 123 */ @loop5 /* 456 */ + + for (i in 1..100) { + break@loop5 + } /* 789 */ + } +} diff --git a/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.data.Sample.kt b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.data.Sample.kt new file mode 100644 index 00000000000..ecfe26cdd51 --- /dev/null +++ b/idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.data.Sample.kt @@ -0,0 +1,5 @@ +fun bar() { + @l while (true) { + break@l + } +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index acc5a736047..13e2e6c0a57 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -918,6 +918,21 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes } } + @TestMetadata("idea/testData/quickfix/migration/obsoleteLabelSyntax") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ObsoleteLabelSyntax extends AbstractQuickFixMultiFileTest { + public void testAllFilesPresentInObsoleteLabelSyntax() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/migration/obsoleteLabelSyntax"), Pattern.compile("^(\\w+)\\.before\\.Main\\.kt$"), true); + } + + @TestMetadata("manyFilesMuitliple.before.Main.kt") + public void testManyFilesMuitliple() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/migration/obsoleteLabelSyntax/manyFilesMuitliple.before.Main.kt"); + doTestWithExtraFile(fileName); + } + } + @TestMetadata("idea/testData/quickfix/migration/removeNameFromFunctionExpression") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 587bf5e0292..4ff29696a11 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -3295,6 +3295,27 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest { } } + @TestMetadata("idea/testData/quickfix/migration/obsoleteLabelSyntax") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ObsoleteLabelSyntax extends AbstractQuickFixTest { + public void testAllFilesPresentInObsoleteLabelSyntax() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/migration/obsoleteLabelSyntax"), Pattern.compile("^(\\w+)\\.kt$"), true); + } + + @TestMetadata("lambda.kt") + public void testLambda() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/migration/obsoleteLabelSyntax/lambda.kt"); + doTest(fileName); + } + + @TestMetadata("loop.kt") + public void testLoop() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/migration/obsoleteLabelSyntax/loop.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/quickfix/migration/removeNameFromFunctionExpression") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)