Add quickfix for deprecated labels syntax

This commit is contained in:
Denis Zharkov
2015-05-18 15:49:44 +03:00
parent e98b9ea84e
commit 87be626848
13 changed files with 380 additions and 0 deletions
@@ -536,6 +536,16 @@ inline fun <reified T : JetElement, R> flatMapDescendantsOfTypeVisitor(
inline fun <reified T : JetElement> PsiElement.forEachDescendantsOfType(noinline block: (T) -> Unit) =
accept(forEachDescendantOfTypeVisitor(block))
inline fun <reified T : JetElement> PsiElement.anyDescendantOfType(noinline predicate: (T) -> Boolean): Boolean {
var result = false
accept(forEachDescendantOfTypeVisitor<T> {
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
@@ -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());
}
}
@@ -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<JetAnnotationEntry>(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<JetAnnotationEntry>() ?: return null
if (!annotationEntry.looksLikeObsoleteLabel()) return null
return ReplaceObsoleteLabelSyntaxFix(annotationEntry)
}
public fun createWholeProjectFixFactory(): JetSingleIntentionActionFactory = createIntentionFactory factory@ {
diagnostic ->
if (!(diagnostic.getPsiElement().getNonStrictParentOfType<JetAnnotationEntry>()?.looksLikeObsoleteLabelWithReferencesInCode()
?: false)) return@factory null
JetWholeProjectForEachElementOfTypeFix.createForMultiTask<JetAnnotatedExpression, JetAnnotationEntry>(
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<JetExpressionWithLabel> {
(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)
}
}
}
@@ -0,0 +1,9 @@
// "Replace with label label@" "true"
fun run(block: () -> Unit) = block()
fun foo() {
run @label<caret> {
return@label
}
}
@@ -0,0 +1,9 @@
// "Replace with label label@" "true"
fun run(block: () -> Unit) = block()
fun foo() {
run label@ {
return@label
}
}
@@ -0,0 +1,7 @@
// "Replace with label loop@" "true"
fun foo() {
@loop<caret> for (i in 1..100) {
break@loop
}
}
@@ -0,0 +1,7 @@
// "Replace with label loop@" "true"
fun foo() {
loop@ for (i in 1..100) {
break@loop
}
}
@@ -0,0 +1,5 @@
fun bar() {
l@ while (true) {
break@l
}
}
@@ -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 */
}
}
@@ -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<caret>
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 */
}
}
@@ -0,0 +1,5 @@
fun bar() {
@l while (true) {
break@l
}
}
@@ -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)
@@ -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)