diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/JetBundle.properties b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/JetBundle.properties index 74c0d571179..c3a94b43096 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/JetBundle.properties +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/JetBundle.properties @@ -192,6 +192,9 @@ migrate.class.object.to.companion.in.whole.project=Replace 'class' keyword with migrate.class.object.to.companion.in.whole.project.family=Replace 'class' Keyword with 'companion' Modifier in Whole Project migrate.lambda.syntax=Migrate lambda syntax migrate.lambda.syntax.family=Migrate lambda syntax +migrate.lambda.syntax.in.whole.project=Migrate lambda syntax in whole project +migrate.lambda.syntax.in.whole.project.modal.title=Migrating lambda syntax in whole project +migrate.lambda.syntax.in.whole.project.family=Migrate lambda syntax in whole project remove.val.var.from.parameter=Remove ''{0}'' from parameter add.override.to.equals.hashCode.toString=Add 'override' to equals, hashCode, toString in project add.when.else.branch.action.family.name=Add Else Branch diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/ApplicationUtils.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/ApplicationUtils.kt index 0e932f65010..ef2ad0e2a25 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/ApplicationUtils.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/util/ApplicationUtils.kt @@ -32,6 +32,10 @@ public fun Project.executeWriteCommand(name: String, command: () -> Unit) { CommandProcessor.getInstance().executeCommand(this, { runWriteAction(command) }, name, null) } +public fun Project.executeCommand(name: String, command: () -> Unit) { + CommandProcessor.getInstance().executeCommand(this, command, name, null) +} + public fun Project.executeWriteCommand(name: String, command: () -> T): T { var result: T = null as T CommandProcessor.getInstance().executeCommand(this, { result = runWriteAction(command) }, name, null) diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt index 7ae3129e200..5b3d563a0d8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedLambdaSyntaxFix.kt @@ -16,35 +16,20 @@ package org.jetbrains.kotlin.idea.quickfix -import com.intellij.codeInsight.intention.IntentionAction -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.diagnostics.DiagnosticUtils -import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.idea.JetBundle import org.jetbrains.kotlin.idea.caches.resolve.analyze -import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully -import org.jetbrains.kotlin.idea.project.PluginJetFilesProvider import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.idea.util.ShortenReferences -import org.jetbrains.kotlin.idea.util.application.runReadAction -import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.psiModificationUtil.getFunctionLiteralArgumentName import org.jetbrains.kotlin.idea.util.psiModificationUtil.moveInsideParenthesesAndReplaceWith import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.getParentOfType -import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypeAndBranch import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.BindingTrace -import org.jetbrains.kotlin.types.JetType -import org.jetbrains.kotlin.types.expressions.FunctionsTypingVisitor -import org.jetbrains.kotlin.utils.addIfNotNull import java.util.ArrayList public class DeprecatedLambdaSyntaxFix(element: JetFunctionLiteralExpression) : JetIntentionAction(element) { @@ -52,7 +37,7 @@ public class DeprecatedLambdaSyntaxFix(element: JetFunctionLiteralExpression) : override fun getFamilyName() = JetBundle.message("migrate.lambda.syntax.family") override fun invoke(project: Project, editor: Editor, file: JetFile) { - LambdaWithDeprecatedSyntax(element, JetPsiFactory(project)).runFix() + DeprecatedSyntaxFix.createFix(element).runFix() } companion object Factory : JetSingleIntentionActionFactory() { @@ -61,43 +46,125 @@ public class DeprecatedLambdaSyntaxFix(element: JetFunctionLiteralExpression) : } } -private class LambdaWithDeprecatedSyntax(val functionLiteralExpression: JetFunctionLiteralExpression, val psiFactory: JetPsiFactory, val level: Int = 0) { - val functionLiteral = functionLiteralExpression.getFunctionLiteral() - val hasNoReturnAndReceiverType = !functionLiteral.hasDeclaredReturnType() && functionLiteral.getReceiverTypeReference() == null +public class DeprecatedLambdaSyntaxInWholeProjectFix(element: JetFunctionLiteralExpression) : + JetWholeProjectModalAction>( + element, JetBundle.message("migrate.lambda.syntax.in.whole.project.modal.title")) { - val bindingContext = if (hasNoReturnAndReceiverType) null else functionLiteralExpression.analyze() - val functionLiteralType: JetType? = if (hasNoReturnAndReceiverType) null else { - val type = bindingContext!!.get(BindingContext.EXPRESSION_TYPE, functionLiteralExpression) - assert(type != null && KotlinBuiltIns.isFunctionOrExtensionFunctionType(type)) { - "Broken function type for expression: ${functionLiteralExpression.getText()}, at: ${DiagnosticUtils.atLocation(functionLiteralExpression)}" - } - type + override fun getText() = JetBundle.message("migrate.lambda.syntax.in.whole.project") + override fun getFamilyName() = JetBundle.message("migrate.lambda.syntax.in.whole.project.family") + + override fun collectDataForFile(project: Project, file: JetFile): Collection? { + val lambdas = ArrayList() + file.accept(LambdaCollectionVisitor(lambdas), 0) + return lambdas.sortBy { -it.level } } + override fun applyChangesForFile(project: Project, file: JetFile, data: Collection) { + data.forEach { + it.runFix() + } + } + + private class LambdaCollectionVisitor(val lambdas: MutableCollection) : JetTreeVisitor() { + override fun visitFunctionLiteralExpression(functionLiteralExpression: JetFunctionLiteralExpression, data: Int): Void? { + functionLiteralExpression.acceptChildren(this, data + 1) + if (JetPsiUtil.isDeprecatedLambdaSyntax(functionLiteralExpression)) { + lambdas.add(DeprecatedSyntaxFix.createFix(functionLiteralExpression, data)) + } + return null + } + + override fun visitJetFile(file: JetFile, data: Int?): Void? { + super.visitJetFile(file, data) + file.acceptChildren(this, data) + return null + } + } + + companion object Factory : JetSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic) + = (diagnostic.getPsiElement() as? JetFunctionLiteralExpression)?.let { DeprecatedLambdaSyntaxInWholeProjectFix(it) } + } +} + +private trait DeprecatedSyntaxFix { + val level: Int + // you must run it under write action - fun runFix() { + fun runFix() + + internal companion object { + fun createFix(functionLiteralExpression: JetFunctionLiteralExpression, level: Int = 0): DeprecatedSyntaxFix { + val functionLiteral = functionLiteralExpression.getFunctionLiteral() + val hasNoReturnAndReceiverType = !functionLiteral.hasDeclaredReturnType() && functionLiteral.getReceiverTypeReference() == null + + return if (hasNoReturnAndReceiverType) DeparenthesizeParameterList(functionLiteralExpression, level) + else LambdaToFunctionExpression(functionLiteralExpression, level) + } + } +} + +private class DeparenthesizeParameterList( + val functionLiteralExpression: JetFunctionLiteralExpression, + override val level: Int = 0 +): DeprecatedSyntaxFix { + + override fun runFix() { if (!JetPsiUtil.isDeprecatedLambdaSyntax(functionLiteralExpression)) return - if (hasNoReturnAndReceiverType) { - removeExternalParenthesesOnParameterList(functionLiteral, psiFactory) + val psiFactory = JetPsiFactory(functionLiteralExpression) + val functionLiteral = functionLiteralExpression.getFunctionLiteral() + val parameterList = functionLiteral.getValueParameterList() + if (parameterList != null && parameterList.isParenthesized()) { + val oldParameterList = parameterList.getText() + val newParameterList = oldParameterList.substring(1..oldParameterList.length() - 2) + parameterList.replace(psiFactory.createFunctionLiteralParameterList(newParameterList)) + } + } +} + +private class LambdaToFunctionExpression( + val functionLiteralExpression: JetFunctionLiteralExpression, + override val level: Int = 0 +): DeprecatedSyntaxFix { + val functionLiteralArgumentName: String? + val receiverType: String? + val returnType: String? + + init { + val bindingContext = functionLiteralExpression.analyze() + val functionLiteralType = bindingContext.get(BindingContext.EXPRESSION_TYPE, functionLiteralExpression) + assert(functionLiteralType != null && KotlinBuiltIns.isFunctionOrExtensionFunctionType(functionLiteralType)) { + "Broken function type for expression: ${functionLiteralExpression.getText()}, at: ${DiagnosticUtils.atLocation(functionLiteralExpression)}" + } + receiverType = KotlinBuiltIns.getReceiverType(functionLiteralType)?.let { IdeDescriptorRenderers.SOURCE_CODE.renderType(it) } + returnType = KotlinBuiltIns.getReturnTypeFromFunctionType(functionLiteralType).let { + if (KotlinBuiltIns.isUnit(it)) + null + else + IdeDescriptorRenderers.SOURCE_CODE.renderType(it) + } + functionLiteralArgumentName = getFunctionLiteralArgument()?.getFunctionLiteralArgumentName(bindingContext) + } + + override fun runFix() { + if (!JetPsiUtil.isDeprecatedLambdaSyntax(functionLiteralExpression)) return + + val newFunctionExpression = createFunctionExpression() + + val literalArgument = getFunctionLiteralArgument() + val replacedFunctionExpression = if (literalArgument == null) { + functionLiteralExpression.replace(newFunctionExpression) } else { - val functionExpression = convertToFunctionExpression(functionLiteralType!!) - - val literalArgument = getFunctionLiteralArgument() - val newFunctionExpression = if (literalArgument == null) { - functionLiteralExpression.replace(functionExpression) as JetNamedFunction - } - else { - literalArgument.moveInsideParenthesesAndReplaceWith(functionExpression, bindingContext!!). - getValueArguments().last().getArgumentExpression() as JetNamedFunction - } - - // todo move outside - ShortenReferences.DEFAULT.process( - listOf(newFunctionExpression.getReceiverTypeReference(), newFunctionExpression.getTypeReference()).filterNotNull() - ) + literalArgument.moveInsideParenthesesAndReplaceWith(newFunctionExpression, functionLiteralArgumentName). + getValueArguments().last().getArgumentExpression() } + + val functionExpression = JetPsiUtil.deparenthesize(replacedFunctionExpression as JetExpression) as JetNamedFunction + + ShortenReferences.DEFAULT.process( + listOf(functionExpression.getReceiverTypeReference(), functionExpression.getTypeReference()).filterNotNull()) } private fun getFunctionLiteralArgument(): JetFunctionLiteralArgument? { @@ -109,15 +176,6 @@ private class LambdaWithDeprecatedSyntax(val functionLiteralExpression: JetFunct return null } - private fun removeExternalParenthesesOnParameterList(functionLiteral: JetFunctionLiteral, psiFactory: JetPsiFactory) { - val parameterList = functionLiteral.getValueParameterList() - if (parameterList != null && parameterList.isParenthesized()) { - val oldParameterList = parameterList.getText() - val newParameterList = oldParameterList.substring(1..oldParameterList.length() - 2) - parameterList.replace(psiFactory.createFunctionLiteralParameterList(newParameterList)) - } - } - private fun JetElement.replaceWithReturn(psiFactory: JetPsiFactory) { if (this is JetReturnExpression) { return @@ -135,18 +193,11 @@ private class LambdaWithDeprecatedSyntax(val functionLiteralExpression: JetFunct return null } - private fun convertToFunctionExpression( - functionLiteralType: JetType - ): JetNamedFunction { + private fun createFunctionExpression(): JetNamedFunction { + val psiFactory = JetPsiFactory(functionLiteralExpression) + val functionLiteral = functionLiteralExpression.getFunctionLiteral() val functionName = getLambdaLabelName() val parameterList = functionLiteral.getValueParameterList()?.getText() - val receiverType = KotlinBuiltIns.getReceiverType(functionLiteralType)?.let { IdeDescriptorRenderers.SOURCE_CODE.renderType(it) } - val returnType = KotlinBuiltIns.getReturnTypeFromFunctionType(functionLiteralType).let { - if (KotlinBuiltIns.isUnit(it)) - null - else - IdeDescriptorRenderers.SOURCE_CODE.renderType(it) - } val functionDeclaration = "fun " + (receiverType?.let { "$it." } ?: "") + diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/JetWholeProjectModalAction.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/JetWholeProjectModalAction.kt new file mode 100644 index 00000000000..9f49f7c680a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/JetWholeProjectModalAction.kt @@ -0,0 +1,90 @@ +/* + * 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.openapi.command.CommandProcessor +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.util.ui.UIUtil +import org.jetbrains.kotlin.idea.project.PluginJetFilesProvider +import org.jetbrains.kotlin.idea.util.application.executeCommand +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.psi.JetFile +import java.util.HashMap + +public abstract class JetWholeProjectModalAction(element: T, val title: String) : JetIntentionAction(element) { + override final fun startInWriteAction() = false + + override final fun invoke(project: Project, editor: Editor, file: JetFile) = + ProgressManager.getInstance().run( + object : Task.Modal(project, title, true) { + override fun run(indicator: ProgressIndicator) { + val filesToData = HashMap() + val files = runReadAction { PluginJetFilesProvider.allFilesInProject(project) } + + for ((i, currentFile) in files.withIndex()) { + indicator.setText("Checking file $i of ${files.size()}...") + indicator.setText2(currentFile.getVirtualFile().getPath()) + indicator.setFraction((i + 1) / files.size().toDouble()) + try { + val data = runReadAction { collectDataForFile(project, currentFile) } + if (data != null) filesToData[currentFile] = data + } + catch (e: ProcessCanceledException) { + return + } + catch (e: Throwable) { + LOG.error(e) + } + } + applyAll(project, filesToData) + } + }) + + private fun applyAll(project: Project, filesToData: Map) { + UIUtil.invokeLaterIfNeeded { + project.executeCommand(getText()) { + filesToData.forEach { + try { + runWriteAction { applyChangesForFile(project, it.getKey(), it.getValue()) } + } + catch (e: Throwable) { + LOG.error(e) + } + } + } + } + } + + // this method will be started under read action + protected abstract fun collectDataForFile(project: Project, file: JetFile): D? + + // this method will be started under write action + protected abstract fun applyChangesForFile(project: Project, file: JetFile, data: D) + + private companion object { + val LOG = Logger.getInstance(javaClass>()); + } +} + diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java index 098bbc67f9c..3a8e32e03de 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.java @@ -233,6 +233,7 @@ public class QuickFixRegistrar { QuickFixes.factories.put(UNUSED_PARAMETER, ChangeFunctionSignatureFix.createFactoryForUnusedParameter()); QuickFixes.factories.put(EXPECTED_PARAMETERS_NUMBER_MISMATCH, ChangeFunctionSignatureFix.createFactoryForParametersNumberMismatch()); QuickFixes.factories.put(DEPRECATED_LAMBDA_SYNTAX, DeprecatedLambdaSyntaxFix.Factory); + QuickFixes.factories.put(DEPRECATED_LAMBDA_SYNTAX, DeprecatedLambdaSyntaxInWholeProjectFix.Factory); QuickFixes.factories.put(EXPECTED_PARAMETER_TYPE_MISMATCH, ChangeTypeFix.createFactoryForExpectedParameterTypeMismatch()); QuickFixes.factories.put(EXPECTED_RETURN_TYPE_MISMATCH, ChangeTypeFix.createFactoryForExpectedReturnTypeMismatch()); diff --git a/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.after.data.Sample.kt b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.after.data.Sample.kt new file mode 100644 index 00000000000..96c94faf368 --- /dev/null +++ b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.after.data.Sample.kt @@ -0,0 +1,8 @@ +val h = { -> } + +val l = @bar ( + fun Int.bar() { + }) + +val s = ( + fun (): Int = 5)() \ No newline at end of file diff --git a/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.after.kt b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.after.kt new file mode 100644 index 00000000000..c80a50780c6 --- /dev/null +++ b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.after.kt @@ -0,0 +1,9 @@ +// "Migrate lambda syntax in whole project" "true" + +val a = + fun (): Int { + val b = + fun (): Int = 5 + + return b() + } \ No newline at end of file diff --git a/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.Main.kt b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.Main.kt new file mode 100644 index 00000000000..7437b1591f8 --- /dev/null +++ b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.Main.kt @@ -0,0 +1,8 @@ +// "Migrate lambda syntax in whole project" "true" + +val a = { (): Int -> + + val b = { (): Int -> 5 } + + b() +} \ No newline at end of file diff --git a/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.data.Sample.kt b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.data.Sample.kt new file mode 100644 index 00000000000..108243d20ef --- /dev/null +++ b/idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.data.Sample.kt @@ -0,0 +1,5 @@ +val h = { () -> } + +val l = @bar { Int.() -> } + +val s = {(): Int -> 5}() \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index 2a02c34a7b6..5a3ea0ccf25 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -857,6 +857,7 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes @TestMetadata("idea/testData/quickfix/migration") @TestDataPath("$PROJECT_ROOT") @InnerTestClasses({ + Migration.LambdaSyntax.class, }) @RunWith(JUnit3RunnerWithInners.class) public static class Migration extends AbstractQuickFixMultiFileTest { @@ -876,6 +877,20 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes doTestWithExtraFile(fileName); } + @TestMetadata("idea/testData/quickfix/migration/lambdaSyntax") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class LambdaSyntax extends AbstractQuickFixMultiFileTest { + public void testAllFilesPresentInLambdaSyntax() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/migration/lambdaSyntax"), Pattern.compile("^(\\w+)\\.before\\.Main\\.kt$"), true); + } + + @TestMetadata("lambdaSyntaxMultiple.before.Main.kt") + public void testLambdaSyntaxMultiple() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/migration/lambdaSyntax/lambdaSyntaxMultiple.before.Main.kt"); + doTestWithExtraFile(fileName); + } + } } @TestMetadata("idea/testData/quickfix/modifiers")