Simplest inline function handler.
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 9c6af23..0812580 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml
@@ -288,6 +288,7 @@ <refactoring.introduceParameterMethodUsagesProcessor implementation="org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.KotlinIntroduceParameterMethodUsageProcessor"/> <inlineActionHandler implementation="org.jetbrains.kotlin.idea.refactoring.inline.KotlinInlineValHandler"/> + <inlineActionHandler implementation="org.jetbrains.kotlin.idea.refactoring.inline.KotlinInlineFunctionHandler"/> <treeStructureProvider implementation="org.jetbrains.kotlin.idea.projectView.JetProjectViewProvider"/> <colorSettingsPage implementation="org.jetbrains.kotlin.idea.highlighter.KotlinColorSettingsPage"/>
diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedSymbolUsageInWholeProjectFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedSymbolUsageInWholeProjectFix.kt index 0b6c4c8..cc8af7c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedSymbolUsageInWholeProjectFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/DeprecatedSymbolUsageInWholeProjectFix.kt
@@ -41,6 +41,7 @@ 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.JetNamedDeclaration import org.jetbrains.kotlin.psi.JetNamedFunction import org.jetbrains.kotlin.psi.JetProperty import org.jetbrains.kotlin.psi.JetSimpleNameExpression @@ -58,8 +59,6 @@ private val text: String ) : DeprecatedSymbolUsageFixBase(element, replaceWith) { - private val LOG = Logger.getInstance(javaClass<DeprecatedSymbolUsageInWholeProjectFix>()); - override fun getFamilyName() = "Replace deprecated symbol usage in whole project" override fun getText() = text @@ -81,73 +80,12 @@ ) { val psiElement = element.getReference()!!.resolve()!! - ProgressManager.getInstance().run( - object : Task.Modal(project, "Applying '$text'", true) { - override fun run(indicator: ProgressIndicator) { - val usages = runReadAction { - val searchScope = JetSourceFilterScope.kotlinSources(GlobalSearchScope.projectScope(project), project) - val findUsagesHandler = KotlinFindUsagesHandlerFactory(project).createFindUsagesHandler(psiElement, false)!! - val processor = CommonProcessors.CollectProcessor<UsageInfo>() - val options = createFindUsagesOptions(psiElement, searchScope, project) - findUsagesHandler.processElementUsages(psiElement, processor, options) - processor.getResults().map { it.getElement() }.filterIsInstance<JetSimpleNameExpression>() - } - replaceUsages(project, usages, replacement) - } - }) - } - - private fun createFindUsagesOptions(element: PsiElement, searchScope: GlobalSearchScope, project: Project): FindUsagesOptions { - val options: FindUsagesOptions = when (element) { - is JetNamedFunction -> { - with(KotlinFunctionFindUsagesOptions(project)) { - isSkipImportStatements = true - isOverridingMethods = false - isImplementingMethods = false - isIncludeInherited = false - isIncludeOverloadUsages = false - this - } - } - - is JetProperty -> { - with(KotlinPropertyFindUsagesOptions(project)) { - isSkipImportStatements = true - this - } - } - - else -> throw IllegalArgumentException(element.toString()) //TODO? - } - - options.searchScope = searchScope - options.isSearchForTextOccurrences = false - return options - } - - private fun replaceUsages(project: Project, usages: Collection<JetSimpleNameExpression>, replacement: ReplaceWithAnnotationAnalyzer.ReplacementExpression) { - UIUtil.invokeLaterIfNeeded { - project.executeCommand(getText()) { - runWriteAction { - for (usage in usages) { - try { - if (!usage.isValid()) continue // TODO: nested calls - val bindingContext = usage.analyze(BodyResolveMode.PARTIAL) - val resolvedCall = usage.getResolvedCall(bindingContext) ?: continue - if (!resolvedCall.getStatus().isSuccess()) continue - // copy replacement expression because it is modified by performReplacement - DeprecatedSymbolUsageFixBase.performReplacement(usage, bindingContext, resolvedCall, replacement.copy()) - } - catch (e: Throwable) { - LOG.error(e) - } - } - } - } - } + findAndReplaceUsages(project, psiElement as JetNamedDeclaration, replacement, text, false) } companion object : JetSingleIntentionActionFactory() { + private val LOG = Logger.getInstance(javaClass<DeprecatedSymbolUsageInWholeProjectFix>()); + //TODO: better rendering needed private val RENDERER = DescriptorRenderer.withOptions { modifiers = emptySet() @@ -168,5 +106,85 @@ return DeprecatedSymbolUsageInWholeProjectFix(nameExpression, replacement, "Replace usages of '$descriptorName' in whole project") } + // TODO extract the following code + public fun findAndReplaceUsages( + project: Project, + declaration: JetNamedDeclaration, + replacement: ReplaceWithAnnotationAnalyzer.ReplacementExpression, + text: String, + deleteDeclaration: Boolean + ) { + ProgressManager.getInstance().run( + object : Task.Modal(project, "Applying '$text'", true) { + override fun run(indicator: ProgressIndicator) { + val usages = runReadAction { + val searchScope = JetSourceFilterScope.kotlinSources(GlobalSearchScope.projectScope(project), project) // TODO maybe use scope instead of project scope? + val findUsagesHandler = KotlinFindUsagesHandlerFactory(project).createFindUsagesHandler(declaration, false)!! + val processor = CommonProcessors.CollectProcessor<UsageInfo>() + val options = createFindUsagesOptions(declaration, searchScope, project) + findUsagesHandler.processElementUsages(declaration, processor, options) + processor.getResults().map { it.getElement() }.filterIsInstance<JetSimpleNameExpression>() + } + replaceUsages(project, usages, replacement, text, if (deleteDeclaration) declaration else null) + } + }) + } + + private fun createFindUsagesOptions(element: PsiElement, searchScope: GlobalSearchScope, project: Project): FindUsagesOptions { + val options: FindUsagesOptions = when (element) { + is JetNamedFunction -> { + with(KotlinFunctionFindUsagesOptions(project)) { + isSkipImportStatements = true + isOverridingMethods = false + isImplementingMethods = false + isIncludeInherited = false + isIncludeOverloadUsages = false + this + } + } + + is JetProperty -> { + with(KotlinPropertyFindUsagesOptions(project)) { + isSkipImportStatements = true + this + } + } + + else -> throw IllegalArgumentException(element.toString()) //TODO? + } + + options.searchScope = searchScope + options.isSearchForTextOccurrences = false + return options + } + + private fun replaceUsages( + project: Project, + usages: Collection<JetSimpleNameExpression>, + replacement: ReplaceWithAnnotationAnalyzer.ReplacementExpression, + commandText: String, + declarationToDelete: JetNamedDeclaration? + ) { + UIUtil.invokeLaterIfNeeded { + project.executeCommand(commandText) { + runWriteAction { + for (usage in usages) { + try { + if (!usage.isValid()) continue // TODO: nested calls + val bindingContext = usage.analyze(BodyResolveMode.PARTIAL) + val resolvedCall = usage.getResolvedCall(bindingContext) ?: continue + if (!resolvedCall.getStatus().isSuccess()) continue + // copy replacement expression because it is modified by performReplacement + DeprecatedSymbolUsageFixBase.performReplacement(usage, bindingContext, resolvedCall, replacement.copy()) + } + catch (e: Throwable) { + LOG.error(e) + } + } + declarationToDelete?.delete() + } + } + } + } } }
diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/inline/KotlinInlineFunctionHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/inline/KotlinInlineFunctionHandler.kt new file mode 100644 index 0000000..0ffd867 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/inline/KotlinInlineFunctionHandler.kt
@@ -0,0 +1,80 @@ +/* + * 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.refactoring.inline + +import com.google.common.collect.Lists +import com.intellij.lang.Language +import com.intellij.lang.refactoring.InlineActionHandler +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.CommandProcessor +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.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.refactoring.RefactoringBundle +import com.intellij.usageView.UsageInfo +import com.intellij.util.CommonProcessors +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.idea.JetLanguage +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.findUsages.KotlinFindUsagesHandlerFactory +import org.jetbrains.kotlin.idea.quickfix.DeprecatedSymbolUsageInWholeProjectFix +import org.jetbrains.kotlin.idea.quickfix.ReplaceWith +import org.jetbrains.kotlin.idea.quickfix.ReplaceWithAnnotationAnalyzer +import org.jetbrains.kotlin.idea.stubindex.JetSourceFilterScope +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getParentOfType +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall + +public class KotlinInlineFunctionHandler: InlineActionHandler() { + override fun isEnabledForLanguage(language: Language) = JetLanguage.INSTANCE == language + + override fun canInlineElement(element: PsiElement): Boolean { + return element is JetNamedFunction + && element.hasBody() && !element.hasBlockBody() // TODO support multiline functions + && element.getUseScope() is GlobalSearchScope // TODO support local functions + } + + override fun inlineElement(project: Project, editor: Editor?, element: PsiElement) { + element as JetNamedFunction + + val descriptor = element.resolveToDescriptor() as SimpleFunctionDescriptor + + val replacement = ReplaceWithAnnotationAnalyzer.analyze( + ReplaceWith(element.getBodyExpression()!!.getText()), + descriptor, + element.getResolutionFacade(), + project + ) + + DeprecatedSymbolUsageInWholeProjectFix.findAndReplaceUsages( + project, + element, + replacement, + RefactoringBundle.message("inline.command", element.getName()), + true + ) + } +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt new file mode 100644 index 0000000..29b8c82 --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt
@@ -0,0 +1,11 @@ +fun <caret>f(p: Int) = g() + +fun g() { +} + +fun complexFun(): Int { +} + +fun main(args: Array<String>) { + f(complexFun()) +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt.after b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt.after new file mode 100644 index 0000000..aef93bb --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt.after
@@ -0,0 +1,10 @@ +fun g() { +} + +fun complexFun(): Int { +} + +fun main(args: Array<String>) { + complexFun() + g() +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt new file mode 100644 index 0000000..1edcabc --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt
@@ -0,0 +1,8 @@ +fun <caret>f(p: Int) = p + p + +fun complexFun(): Int { +} + +fun main(args: Array<String>) { + f(complexFun()) +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt.after b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt.after new file mode 100644 index 0000000..0ee242e --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt.after
@@ -0,0 +1,7 @@ +fun complexFun(): Int { +} + +fun main(args: Array<String>) { + val p = complexFun() + p + p +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/Constant.kt b/idea/testData/refactoring/inline/function/expressionBody/Constant.kt new file mode 100644 index 0000000..cff2731 --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/Constant.kt
@@ -0,0 +1,5 @@ +fun <caret>f() = "foo" + +fun main(args: Array<String>) { + println(f() + f()) +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/Constant.kt.after b/idea/testData/refactoring/inline/function/expressionBody/Constant.kt.after new file mode 100644 index 0000000..cddff1f --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/Constant.kt.after
@@ -0,0 +1,3 @@ +fun main(args: Array<String>) { + println("foo" + "foo") +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt b/idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt new file mode 100644 index 0000000..c9d27fd --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt
@@ -0,0 +1,10 @@ +fun <caret>foo(p1: String, p2: () -> Boolean) = bar(p1, null, p2) + +fun bar(p1: String, p2: String?, p3: () -> Boolean) + +fun foo(i: I) { + foo("foo") { + println("bar") + println("baz") + } +}
diff --git a/idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt.after b/idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt.after new file mode 100644 index 0000000..ca809de --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt.after
@@ -0,0 +1,8 @@ +fun bar(p1: String, p2: String?, p3: () -> Boolean) + +fun foo(i: I) { + bar("foo", null) { + println("bar") + println("baz") + } +}
diff --git a/idea/testData/refactoring/inline/function/expressionBody/Simple.kt b/idea/testData/refactoring/inline/function/expressionBody/Simple.kt new file mode 100644 index 0000000..f2b491d --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/Simple.kt
@@ -0,0 +1,5 @@ +fun <caret>f(p1: Int, p2: Int) = p1 + p2 + +fun main(args: Array<String>) { + println(f(3, 5)) +} \ No newline at end of file
diff --git a/idea/testData/refactoring/inline/function/expressionBody/Simple.kt.after b/idea/testData/refactoring/inline/function/expressionBody/Simple.kt.after new file mode 100644 index 0000000..e1e0a92 --- /dev/null +++ b/idea/testData/refactoring/inline/function/expressionBody/Simple.kt.after
@@ -0,0 +1,3 @@ +fun main(args: Array<String>) { + println(3 + 5) +} \ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/AbstractInlineTest.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/AbstractInlineTest.java index 19e9366..0bb2e03 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/AbstractInlineTest.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/AbstractInlineTest.java
@@ -16,13 +16,16 @@ package org.jetbrains.kotlin.idea.refactoring.inline; -import com.intellij.codeInsight.TargetElementUtilBase; +import com.intellij.codeInsight.TargetElementUtil; +import com.intellij.lang.refactoring.InlineActionHandler; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.util.io.FileUtil; import com.intellij.psi.PsiElement; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.testFramework.LightProjectDescriptor; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.idea.test.JetLightCodeInsightFixtureTestCase; import org.jetbrains.kotlin.idea.test.JetWithJdkAndRuntimeLightProjectDescriptor; import org.jetbrains.kotlin.test.InTextDirectivesUtils; @@ -35,6 +38,17 @@ import static com.intellij.codeInsight.TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED; public abstract class AbstractInlineTest extends JetLightCodeInsightFixtureTestCase { + @Nullable + private static InlineActionHandler findSuitableHandler(@NotNull PsiElement psiElement) { + InlineActionHandler[] inlineActionHandlers = Extensions.getExtensions(InlineActionHandler.EP_NAME); + for (InlineActionHandler handler : inlineActionHandlers) { + if (handler.canInlineElement(psiElement)) { + return handler; + } + } + return null; + } + protected void doTest(@NotNull String path) throws IOException { File afterFile = new File(path + ".after"); @@ -43,11 +57,14 @@ boolean afterFileExists = afterFile.exists(); final PsiElement targetElement = - TargetElementUtilBase.findTargetElement(myFixture.getEditor(), ELEMENT_NAME_ACCEPTED | REFERENCED_ELEMENT_ACCEPTED); - final KotlinInlineValHandler handler = new KotlinInlineValHandler(); + TargetElementUtil.findTargetElement(myFixture.getEditor(), ELEMENT_NAME_ACCEPTED | REFERENCED_ELEMENT_ACCEPTED); + + assertNotNull(targetElement); + + final InlineActionHandler handler = findSuitableHandler(targetElement); List<String> expectedErrors = InTextDirectivesUtils.findLinesWithPrefixesRemoved(myFixture.getFile().getText(), "// ERROR: "); - if (handler.canInlineElement(targetElement)) { + if (handler != null) { try { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override
diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/InlineTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/InlineTestGenerated.java index ce13cad..46b73eb 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/InlineTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/InlineTestGenerated.java
@@ -416,6 +416,54 @@ } } + @TestMetadata("idea/testData/refactoring/inline/function") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Function extends AbstractInlineTest { + public void testAllFilesPresentInFunction() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/inline/function"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("idea/testData/refactoring/inline/function/expressionBody") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ExpressionBody extends AbstractInlineTest { + public void testAllFilesPresentInExpressionBody() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/inline/function/expressionBody"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("ComplexArgumentNotUsed.kt") + public void testComplexArgumentNotUsed() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentNotUsed.kt"); + doTest(fileName); + } + + @TestMetadata("ComplexArgumentUsedTwice.kt") + public void testComplexArgumentUsedTwice() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/expressionBody/ComplexArgumentUsedTwice.kt"); + doTest(fileName); + } + + @TestMetadata("Constant.kt") + public void testConstant() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/expressionBody/Constant.kt"); + doTest(fileName); + } + + @TestMetadata("FunctionalParameterPassed.kt") + public void testFunctionalParameterPassed() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/expressionBody/FunctionalParameterPassed.kt"); + doTest(fileName); + } + + @TestMetadata("Simple.kt") + public void testSimple() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/expressionBody/Simple.kt"); + doTest(fileName); + } + } + } + @TestMetadata("idea/testData/refactoring/inline/property") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)