From 8af173bd4b4268385ed4d6b8e1c681653d3ae282 Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Mon, 28 Jul 2014 16:18:46 +0400 Subject: [PATCH] Extract Function: Refactoring: Introduce ExtractionGeneratorOptions and ExtractionResult --- .../extractFunctionForDebuggerUtil.kt | 7 +- .../ExtractKotlinFunctionHandler.kt | 12 +- ...riptor.kt => ExtractableCodeDescriptor.kt} | 22 +- ...ionUtils.kt => extractableAnalysisUtil.kt} | 270 +--------------- .../extractFunction/extractorUtil.kt | 296 ++++++++++++++++++ .../ui/KotlinExtractFunctionDialog.java | 19 +- 6 files changed, 345 insertions(+), 281 deletions(-) rename idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/{ExtractionDescriptor.kt => ExtractableCodeDescriptor.kt} (94%) rename idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/{extractFunctionUtils.kt => extractableAnalysisUtil.kt} (75%) create mode 100644 idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractorUtil.kt diff --git a/idea/src/org/jetbrains/jet/plugin/debugger/evaluate/extractFunctionForDebuggerUtil.kt b/idea/src/org/jetbrains/jet/plugin/debugger/evaluate/extractFunctionForDebuggerUtil.kt index d8b9629d0f8..76d7c855806 100644 --- a/idea/src/org/jetbrains/jet/plugin/debugger/evaluate/extractFunctionForDebuggerUtil.kt +++ b/idea/src/org/jetbrains/jet/plugin/debugger/evaluate/extractFunctionForDebuggerUtil.kt @@ -38,6 +38,7 @@ import com.intellij.psi.impl.PsiModificationTrackerImpl import org.jetbrains.jet.lang.psi.* import org.jetbrains.jet.plugin.intentions.InsertExplicitTypeArguments import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionGeneratorOptions fun getFunctionForExtractedFragment( codeFragment: JetCodeFragment, @@ -96,10 +97,12 @@ fun getFunctionForExtractedFragment( val validationResult = analysisResult.descriptor!!.validate() if (!validationResult.conflicts.isEmpty()) { - throw EvaluateExceptionUtil.createEvaluateException("Following declarations are unavailable in debug scope: ${validationResult.conflicts.keySet().map { it.getText() }.joinToString(",")}") + throw EvaluateExceptionUtil.createEvaluateException("Following declarations are unavailable in debug scope: ${validationResult.conflicts.keySet()?.map { it.getText() }?.makeString(",")}") } - return validationResult.descriptor.generateFunction(true) + return validationResult.descriptor + .generateFunction(ExtractionGeneratorOptions(inTempFile = true)) + .function } return runReadAction { generateFunction() } diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt index c5c42d6fa7b..736b9accac7 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractKotlinFunctionHandler.kt @@ -73,7 +73,7 @@ public class ExtractKotlinFunctionHandler(public val allContainersEnabled: Boole file: JetFile, elements: List, targetSibling: PsiElement, - preprocessor: ((ExtractionDescriptor) -> Unit)? = null + preprocessor: ((ExtractableCodeDescriptor) -> Unit)? = null ) { val project = file.getProject() @@ -83,19 +83,21 @@ public class ExtractKotlinFunctionHandler(public val allContainersEnabled: Boole throw ConflictsInTestsException(analysisResult.messages.map { it.renderMessage() }) } - fun doRefactor(descriptor: ExtractionDescriptor) { + fun doRefactor(descriptor: ExtractableCodeDescriptor, generatorOptions: ExtractionGeneratorOptions) { preprocessor?.invoke(descriptor) - project.executeWriteCommand(EXTRACT_FUNCTION) { descriptor.generateFunction() } + project.executeWriteCommand(EXTRACT_FUNCTION) { descriptor.generateFunction(generatorOptions) } } fun validateAndRefactor() { val validationResult = analysisResult.descriptor!!.validate() project.checkConflictsInteractively(validationResult.conflicts) { if (ApplicationManager.getApplication()!!.isUnitTestMode()) { - doRefactor(validationResult.descriptor) + doRefactor(validationResult.descriptor, ExtractionGeneratorOptions.DEFAULT) } else { - KotlinExtractFunctionDialog(project, validationResult) { doRefactor(it.getCurrentDescriptor()) }.show() + KotlinExtractFunctionDialog(project, validationResult) { + doRefactor(it.getCurrentDescriptor(), it.getGeneratorOptions()) + }.show() } } } diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractionDescriptor.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractableCodeDescriptor.kt similarity index 94% rename from idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractionDescriptor.kt rename to idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractableCodeDescriptor.kt index 41b3adefb11..b40eeb31307 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractionDescriptor.kt +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ExtractableCodeDescriptor.kt @@ -38,6 +38,7 @@ import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor import org.jetbrains.jet.lang.psi.JetProperty import org.jetbrains.jet.lang.psi.JetDeclaration import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.jet.lang.psi.JetNamedFunction trait Parameter { val argumentText: String @@ -152,7 +153,7 @@ class Initializer( override val declarationsToCopy: List ): ControlFlow -data class ExtractionDescriptor( +data class ExtractableCodeDescriptor( val extractionData: ExtractionData, val name: String, val visibility: String, @@ -163,8 +164,21 @@ data class ExtractionDescriptor( val controlFlow: ControlFlow ) +class ExtractionGeneratorOptions( + val inTempFile: Boolean = false +) { + class object { + val DEFAULT = ExtractionGeneratorOptions() + } +} + +data class ExtractionResult( + val function: JetNamedFunction, + val nameByOffset: Map +) + class AnalysisResult ( - val descriptor: ExtractionDescriptor?, + val descriptor: ExtractableCodeDescriptor?, val status: Status, val messages: List ) { @@ -214,7 +228,7 @@ class AnalysisResult ( } } -class ExtractionDescriptorWithConflicts( - val descriptor: ExtractionDescriptor, +class ExtractableCodeDescriptorWithConflicts( + val descriptor: ExtractableCodeDescriptor, val conflicts: MultiMap ) diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractFunctionUtils.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractableAnalysisUtil.kt similarity index 75% rename from idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractFunctionUtils.kt rename to idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractableAnalysisUtil.kt index 752479772c4..9bbb55e9e97 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractFunctionUtils.kt +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractableAnalysisUtil.kt @@ -34,7 +34,6 @@ import org.jetbrains.jet.lang.resolve.scopes.receivers.ThisReceiver import org.jetbrains.jet.lang.cfg.pseudocode.* import org.jetbrains.jet.lang.resolve.BindingContext import org.jetbrains.jet.lang.cfg.Label -import org.jetbrains.jet.lang.psi.JetPsiFactory.FunctionBuilder import org.jetbrains.jet.plugin.refactoring.JetNameValidatorImpl import org.jetbrains.jet.plugin.codeInsight.DescriptorToDeclarationUtil import org.jetbrains.jet.plugin.imports.canBeReferencedViaImport @@ -44,14 +43,10 @@ import org.jetbrains.jet.lang.descriptors.impl.LocalVariableDescriptor import org.jetbrains.jet.utils.DFS import org.jetbrains.jet.utils.DFS.* import org.jetbrains.jet.plugin.caches.resolve.getLazyResolveSession -import org.jetbrains.jet.lang.psi.psiUtil.prependElement -import org.jetbrains.jet.lang.psi.psiUtil.appendElement -import org.jetbrains.jet.plugin.codeInsight.ShortenReferences import com.intellij.refactoring.util.RefactoringUIUtil import com.intellij.util.containers.MultiMap import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache import org.jetbrains.jet.lang.diagnostics.Errors -import org.jetbrains.jet.lang.psi.psiUtil.replaced import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.ErrorMessage import org.jetbrains.jet.lang.cfg.pseudocode.instructions.special.LocalFunctionDeclarationInstruction @@ -62,11 +57,9 @@ import kotlin.properties.Delegates import org.jetbrains.jet.lang.cfg.pseudocodeTraverser.traverse import org.jetbrains.jet.lang.cfg.pseudocodeTraverser.TraversalOrder import org.jetbrains.jet.lang.resolve.bindingContextUtil.getTargetFunctionDescriptor -import com.intellij.psi.PsiWhiteSpace import org.jetbrains.jet.lang.resolve.OverridingUtil import org.jetbrains.jet.lang.resolve.bindingContextUtil.isUsedAsStatement import org.jetbrains.jet.lang.psi.psiUtil.isAncestor -import org.jetbrains.jet.plugin.intentions.declarations.DeclarationUtils import org.jetbrains.jet.lang.resolve.DescriptorToSourceUtils import org.jetbrains.jet.lang.psi.psiUtil.isFunctionLiteralOutsideParentheses import org.jetbrains.jet.plugin.util.psiModificationUtil.moveInsideParenthesesAndReplaceWith @@ -668,7 +661,7 @@ fun ExtractionData.performAnalysis(): AnalysisResult { receiverParameter?.let { adjustedParameters.remove(it) } return AnalysisResult( - ExtractionDescriptor( + ExtractableCodeDescriptor( this, functionName, "", @@ -683,18 +676,17 @@ fun ExtractionData.performAnalysis(): AnalysisResult { ) } -fun ExtractionDescriptor.validate(): ExtractionDescriptorWithConflicts { +fun ExtractableCodeDescriptor.validate(): ExtractableCodeDescriptorWithConflicts { val conflicts = MultiMap() - val nameByOffset = HashMap() - val function = generateFunction(true, nameByOffset) + val result = generateFunction(ExtractionGeneratorOptions(inTempFile = true)) - val bindingContext = AnalyzerFacadeWithCache.getContextForElement(function.getBodyExpression()!!) + val bindingContext = AnalyzerFacadeWithCache.getContextForElement(result.function.getBodyExpression()!!) for ((originalOffset, resolveResult) in extractionData.refOffsetToDeclaration) { if (resolveResult.declaration.isInsideOf(extractionData.originalElements)) continue - val currentRefExpr = nameByOffset[originalOffset] as JetSimpleNameExpression? + val currentRefExpr = result.nameByOffset[originalOffset] as JetSimpleNameExpression? if (currentRefExpr == null) continue if (currentRefExpr.getParent() is JetThisExpression) continue @@ -704,7 +696,7 @@ fun ExtractionDescriptor.validate(): ExtractionDescriptorWithConflicts { val currentDescriptor = bindingContext[BindingContext.REFERENCE_TARGET, currentRefExpr] val currentTarget = currentDescriptor?.let { DescriptorToDeclarationUtil.getDeclaration(extractionData.project, it) } as? PsiNamedElement - if (currentTarget is JetParameter && currentTarget.getParent() == function.getValueParameterList()) continue + if (currentTarget is JetParameter && currentTarget.getParent() == result.function.getValueParameterList()) continue if (currentDescriptor is LocalVariableDescriptor && parameters.any { it.mirrorVarName == currentDescriptor.getName().asString() }) continue @@ -733,7 +725,7 @@ fun ExtractionDescriptor.validate(): ExtractionDescriptorWithConflicts { } } - return ExtractionDescriptorWithConflicts(this, conflicts) + return ExtractableCodeDescriptorWithConflicts(this, conflicts) } private fun comparePossiblyOverridingDescriptors(currentDescriptor: DeclarationDescriptor?, originalDescriptor: DeclarationDescriptor?): Boolean { @@ -745,251 +737,3 @@ private fun comparePossiblyOverridingDescriptors(currentDescriptor: DeclarationD return false } -fun ExtractionDescriptor.getFunctionText( - withBody: Boolean = true, - descriptorRenderer: DescriptorRenderer = DescriptorRenderer.FQ_NAMES_IN_TYPES -): String { - return FunctionBuilder().let { builder -> - builder.modifier(visibility) - - builder.typeParams(typeParameters.map { it.originalDeclaration.getText()!! }) - - receiverParameter?.let { builder.receiver(descriptorRenderer.renderType(it.parameterType)) } - - builder.name(name) - - parameters.forEach { parameter -> - builder.param(parameter.name, descriptorRenderer.renderType(parameter.parameterType)) - } - - with(controlFlow.returnType) { - if (isDefault() || isError()) builder.noReturnType() else builder.returnType(descriptorRenderer.renderType(this)) - } - - builder.typeConstraints(typeParameters.flatMap { it.originalConstraints }.map { it.getText()!! }) - - if (withBody) { - builder.blockBody(extractionData.getCodeFragmentText()) - } - - builder.toFunctionText() - } -} - -fun createNameCounterpartMap(from: JetElement, to: JetElement): Map { - val map = HashMap() - - val fromOffset = from.getTextRange()!!.getStartOffset() - from.accept( - object: JetTreeVisitorVoid() { - override fun visitSimpleNameExpression(expression: JetSimpleNameExpression) { - val offset = expression.getTextRange()!!.getStartOffset() - fromOffset - val newExpression = to.findElementAt(offset)?.getParentByType(javaClass()) - assert(newExpression!= null, "Couldn't find expression at $offset in '${to.getText()}'") - - map[expression] = newExpression!! - } - } - ) - - return map -} - -fun ExtractionDescriptor.generateFunction( - inTempFile: Boolean = false, - nameByOffset: MutableMap = HashMap() -): JetNamedFunction { - val psiFactory = JetPsiFactory(extractionData.originalFile) - fun createFunction(): JetNamedFunction { - return with(extractionData) { - if (inTempFile) { - createTemporaryFunction("${getFunctionText()}\n") - } - else { - psiFactory.createFunction(getFunctionText()) - } - } - } - - fun adjustFunctionBody(function: JetNamedFunction) { - val body = function.getBodyExpression() as JetBlockExpression - - val exprReplacementMap = HashMap JetElement>() - val originalOffsetByExpr = LinkedHashMap() - - val bodyOffset = body.getBlockContentOffset() - val file = body.getContainingFile()!! - - /* - * Sort by descending position so that internals of value/type arguments in calls and qualified types are replaced - * before calls/types themselves - */ - for ((offsetInBody, resolveResult) in extractionData.refOffsetToDeclaration.entrySet().sortDescendingBy { it.key }) { - val expr = file.findElementAt(bodyOffset + offsetInBody)?.getParentByType(javaClass()) - assert(expr != null, "Couldn't find expression at $offsetInBody in '${body.getText()}'") - - originalOffsetByExpr[expr!!] = offsetInBody - - replacementMap[offsetInBody]?.let { replacement -> - if (replacement !is ParameterReplacement || replacement.parameter != receiverParameter) { - exprReplacementMap[expr] = replacement - } - } - } - - val replacingReturn: JetExpression? - val expressionsToReplaceWithReturn: List - if (controlFlow is JumpBasedControlFlow) { - replacingReturn = psiFactory.createExpression(if (controlFlow is ConditionalJump) "return true" else "return") - expressionsToReplaceWithReturn = controlFlow.elementsToReplace.map { jumpElement -> - val offsetInBody = jumpElement.getTextRange()!!.getStartOffset() - extractionData.originalStartOffset!! - val expr = file.findElementAt(bodyOffset + offsetInBody)?.getParentByType(jumpElement.javaClass) - assert(expr != null, "Couldn't find expression at $offsetInBody in '${body.getText()}'") - - expr!! - } - } - else { - replacingReturn = null - expressionsToReplaceWithReturn = Collections.emptyList() - } - - if (replacingReturn != null) { - for (expr in expressionsToReplaceWithReturn) { - expr.replace(replacingReturn) - } - } - - for ((expr, originalOffset) in originalOffsetByExpr) { - if (expr.isValid()) { - nameByOffset.put(originalOffset, exprReplacementMap[expr]?.invoke(expr) ?: expr) - } - } - - for (param in parameters) { - param.mirrorVarName?.let { varName -> - body.prependElement(psiFactory.createProperty(varName, null, true, param.name)) - } - } - - when (controlFlow) { - is ParameterUpdate -> - body.appendElement(psiFactory.createReturn(controlFlow.parameter.nameForRef)) - - is Initializer -> - body.appendElement(psiFactory.createReturn(controlFlow.initializedDeclaration.getName()!!)) - - is ConditionalJump -> - body.appendElement(psiFactory.createReturn("false")) - - is ExpressionEvaluation -> - body.getStatements().last?.let { - val newExpr = it.replaced(psiFactory.createReturn(it.getText() ?: throw AssertionError("Return expression shouldn't be empty: code fragment = ${body.getText()}"))).getReturnedExpression()!! - val counterpartMap = createNameCounterpartMap(it, newExpr) - nameByOffset.entrySet().forEach { e -> counterpartMap[e.getValue()]?.let { e.setValue(it) } } - } - } - } - - fun insertFunction(function: JetNamedFunction): JetNamedFunction { - return with(extractionData) { - val targetContainer = targetSibling.getParent()!! - val emptyLines = psiFactory.createWhiteSpace("\n\n") - if (insertBefore) { - val functionInFile = targetContainer.addBefore(function, targetSibling) as JetNamedFunction - targetContainer.addBefore(emptyLines, targetSibling) - - functionInFile - } - else { - val functionInFile = targetContainer.addAfter(function, targetSibling) as JetNamedFunction - targetContainer.addAfter(emptyLines, targetSibling) - - functionInFile - } - } - } - - fun insertCall(anchor: PsiElement, wrappedCall: JetExpression?) { - if (wrappedCall == null) { - anchor.delete() - return - } - - val firstExpression = extractionData.getExpressions().firstOrNull() - if (firstExpression?.isFunctionLiteralOutsideParentheses() ?: false) { - val functionLiteralArgument = PsiTreeUtil.getParentOfType(firstExpression, javaClass())!! - //todo use the right binding context - functionLiteralArgument.moveInsideParenthesesAndReplaceWith(wrappedCall, BindingContext.EMPTY) - return - } - anchor.replace(wrappedCall) - } - - fun makeCall(function: JetNamedFunction): JetNamedFunction { - val anchor = extractionData.originalElements.first - if (anchor == null) return function - - val anchorParent = anchor.getParent()!! - - anchor.getNextSibling()?.let { from -> - val to = extractionData.originalElements.last - if (to != anchor) { - anchorParent.deleteChildRange(from, to); - } - } - - val callText = parameters - .map { it.argumentText } - .joinToString(separator = ", ", prefix = "$name(", postfix = ")") - - val copiedDeclarations = HashMap() - for (decl in controlFlow.declarationsToCopy) { - val declCopy = psiFactory.createDeclaration(decl.getText()!!) - copiedDeclarations[decl] = anchorParent.addBefore(declCopy, anchor) as JetDeclaration - anchorParent.addBefore(psiFactory.createNewLine(), anchor) - } - - val wrappedCall = when (controlFlow) { - is ExpressionEvaluationWithCallSiteReturn -> - psiFactory.createReturn(callText) - - is ParameterUpdate -> - psiFactory.createExpression("${controlFlow.parameter.argumentText} = $callText") - - is Initializer -> { - val newDecl = copiedDeclarations[controlFlow.initializedDeclaration] as JetProperty - newDecl.replace(DeclarationUtils.changePropertyInitializer(newDecl, psiFactory.createExpression(callText))) - null - } - - is ConditionalJump -> - psiFactory.createExpression("if ($callText) ${controlFlow.elementToInsertAfterCall.getText()}") - - is UnconditionalJump -> { - anchorParent.addAfter( - psiFactory.createExpression(controlFlow.elementToInsertAfterCall.getText()!!), - anchor - ) - anchorParent.addAfter(psiFactory.createNewLine(), anchor) - - psiFactory.createExpression(callText) - } - - else -> - psiFactory.createExpression(callText) - } - insertCall(anchor, wrappedCall) - - return function - } - - val function = createFunction() - adjustFunctionBody(function) - - if (inTempFile) return function - - val functionInPlace = makeCall(insertFunction(function)) - ShortenReferences.process(functionInPlace) - return functionInPlace -} \ No newline at end of file diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractorUtil.kt b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractorUtil.kt new file mode 100644 index 00000000000..642f60c82f3 --- /dev/null +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/extractorUtil.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2010-2014 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.jet.plugin.refactoring.extractFunction + +import org.jetbrains.jet.renderer.DescriptorRenderer +import org.jetbrains.jet.lang.psi.JetPsiFactory.FunctionBuilder +import org.jetbrains.jet.lang.psi.JetElement +import org.jetbrains.jet.lang.psi.JetSimpleNameExpression +import java.util.HashMap +import org.jetbrains.jet.lang.psi.JetTreeVisitorVoid +import org.jetbrains.jet.lang.psi.psiUtil.getParentByType +import org.jetbrains.jet.lang.psi.JetNamedFunction +import org.jetbrains.jet.lang.psi.JetPsiFactory +import org.jetbrains.jet.lang.psi.JetBlockExpression +import java.util.LinkedHashMap +import org.jetbrains.jet.lang.psi.JetExpression +import java.util.Collections +import org.jetbrains.jet.lang.psi.psiUtil.prependElement +import org.jetbrains.jet.lang.psi.psiUtil.appendElement +import org.jetbrains.jet.lang.psi.psiUtil.replaced +import com.intellij.psi.PsiElement +import org.jetbrains.jet.lang.psi.JetCallExpression +import com.intellij.psi.PsiWhiteSpace +import org.jetbrains.jet.lang.psi.JetDeclaration +import org.jetbrains.jet.lang.psi.JetProperty +import org.jetbrains.jet.plugin.intentions.declarations.DeclarationUtils +import org.jetbrains.jet.plugin.codeInsight.ShortenReferences +import org.jetbrains.jet.lang.psi.psiUtil.isFunctionLiteralOutsideParentheses +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.jet.lang.psi.JetFunctionLiteralArgument +import org.jetbrains.jet.lang.resolve.BindingContext +import org.jetbrains.jet.plugin.util.psiModificationUtil.moveInsideParenthesesAndReplaceWith + +fun ExtractableCodeDescriptor.getFunctionText( + withBody: Boolean = true, + descriptorRenderer: DescriptorRenderer = DescriptorRenderer.FQ_NAMES_IN_TYPES +): String { + return FunctionBuilder().let { builder -> + builder.modifier(visibility) + + builder.typeParams(typeParameters.map { it.originalDeclaration.getText()!! }) + + receiverParameter?.let { builder.receiver(descriptorRenderer.renderType(it.parameterType)) } + + builder.name(name) + + parameters.forEach { parameter -> + builder.param(parameter.name, descriptorRenderer.renderType(parameter.parameterType)) + } + + with(controlFlow.returnType) { + if (isDefault() || isError()) builder.noReturnType() else builder.returnType(descriptorRenderer.renderType(this)) + } + + builder.typeConstraints(typeParameters.flatMap { it.originalConstraints }.map { it.getText()!! }) + + if (withBody) { + builder.blockBody(extractionData.getCodeFragmentText()) + } + + builder.toFunctionText() + } +} + +fun createNameCounterpartMap(from: JetElement, to: JetElement): Map { + val map = HashMap() + + val fromOffset = from.getTextRange()!!.getStartOffset() + from.accept( + object: JetTreeVisitorVoid() { + override fun visitSimpleNameExpression(expression: JetSimpleNameExpression) { + val offset = expression.getTextRange()!!.getStartOffset() - fromOffset + val newExpression = to.findElementAt(offset)?.getParentByType(javaClass()) + assert(newExpression!= null, "Couldn't find expression at $offset in '${to.getText()}'") + + map[expression] = newExpression!! + } + } + ) + + return map +} + +fun ExtractableCodeDescriptor.generateFunction(options: ExtractionGeneratorOptions): ExtractionResult { + val psiFactory = JetPsiFactory(extractionData.originalFile) + val nameByOffset = HashMap() + + fun createFunction(): JetNamedFunction { + return with(extractionData) { + if (options.inTempFile) { + createTemporaryFunction("${getFunctionText()}\n") + } + else { + psiFactory.createFunction(getFunctionText()) + } + } + } + + fun adjustFunctionBody(function: JetNamedFunction) { + val body = function.getBodyExpression() as JetBlockExpression + + val exprReplacementMap = HashMap JetElement>() + val originalOffsetByExpr = LinkedHashMap() + + val bodyOffset = body.getBlockContentOffset() + val file = body.getContainingFile()!! + + /* + * Sort by descending position so that internals of value/type arguments in calls and qualified types are replaced + * before calls/types themselves + */ + for ((offsetInBody, resolveResult) in extractionData.refOffsetToDeclaration.entrySet().sortDescendingBy { it.key }) { + val expr = file.findElementAt(bodyOffset + offsetInBody)?.getParentByType(javaClass()) + assert(expr != null, "Couldn't find expression at $offsetInBody in '${body.getText()}'") + + originalOffsetByExpr[expr!!] = offsetInBody + + replacementMap[offsetInBody]?.let { replacement -> + if (replacement !is ParameterReplacement || replacement.parameter != receiverParameter) { + exprReplacementMap[expr] = replacement + } + } + } + + val replacingReturn: JetExpression? + val expressionsToReplaceWithReturn: List + if (controlFlow is JumpBasedControlFlow) { + replacingReturn = psiFactory.createExpression(if (controlFlow is ConditionalJump) "return true" else "return") + expressionsToReplaceWithReturn = controlFlow.elementsToReplace.map { jumpElement -> + val offsetInBody = jumpElement.getTextRange()!!.getStartOffset() - extractionData.originalStartOffset!! + val expr = file.findElementAt(bodyOffset + offsetInBody)?.getParentByType(jumpElement.javaClass) + assert(expr != null, "Couldn't find expression at $offsetInBody in '${body.getText()}'") + + expr!! + } + } + else { + replacingReturn = null + expressionsToReplaceWithReturn = Collections.emptyList() + } + + if (replacingReturn != null) { + for (expr in expressionsToReplaceWithReturn) { + expr.replace(replacingReturn) + } + } + + for ((expr, originalOffset) in originalOffsetByExpr) { + if (expr.isValid()) { + nameByOffset.put(originalOffset, exprReplacementMap[expr]?.invoke(expr) ?: expr) + } + } + + for (param in parameters) { + param.mirrorVarName?.let { varName -> + body.prependElement(psiFactory.createProperty(varName, null, true, param.name)) + } + } + + when (controlFlow) { + is ParameterUpdate -> + body.appendElement(psiFactory.createReturn(controlFlow.parameter.nameForRef)) + + is Initializer -> + body.appendElement(psiFactory.createReturn(controlFlow.initializedDeclaration.getName()!!)) + + is ConditionalJump -> + body.appendElement(psiFactory.createReturn("false")) + + is ExpressionEvaluation -> + body.getStatements().last?.let { + val newExpr = it.replaced(psiFactory.createReturn(it.getText() ?: throw AssertionError("Return expression shouldn't be empty: code fragment = ${body.getText()}"))).getReturnedExpression()!! + val counterpartMap = createNameCounterpartMap(it, newExpr) + nameByOffset.entrySet().forEach { e -> counterpartMap[e.getValue()]?.let { e.setValue(it) } } + } + } + } + + fun insertFunction(function: JetNamedFunction): JetNamedFunction { + return with(extractionData) { + val targetContainer = targetSibling.getParent()!! + val emptyLines = psiFactory.createWhiteSpace("\n\n") + if (insertBefore) { + val functionInFile = targetContainer.addBefore(function, targetSibling) as JetNamedFunction + targetContainer.addBefore(emptyLines, targetSibling) + + functionInFile + } + else { + val functionInFile = targetContainer.addAfter(function, targetSibling) as JetNamedFunction + targetContainer.addAfter(emptyLines, targetSibling) + + functionInFile + } + } + } + + fun insertCall(anchor: PsiElement, wrappedCall: JetExpression?) { + if (wrappedCall == null) { + anchor.delete() + return + } + + val firstExpression = extractionData.getExpressions().firstOrNull() + if (firstExpression?.isFunctionLiteralOutsideParentheses() ?: false) { + val functionLiteralArgument = PsiTreeUtil.getParentOfType(firstExpression, javaClass())!! + //todo use the right binding context + functionLiteralArgument.moveInsideParenthesesAndReplaceWith(wrappedCall, BindingContext.EMPTY) + return + } + anchor.replace(wrappedCall) + } + + fun makeCall(function: JetNamedFunction): JetNamedFunction { + val anchor = extractionData.originalElements.first + if (anchor == null) return function + + val anchorParent = anchor.getParent()!! + + anchor.getNextSibling()?.let { from -> + val to = extractionData.originalElements.last + if (to != anchor) { + anchorParent.deleteChildRange(from, to); + } + } + + val callText = parameters + .map { it.argumentText } + .joinToString(separator = ", ", prefix = "${name}(", postfix = ")") + + val copiedDeclarations = HashMap() + for (decl in controlFlow.declarationsToCopy) { + val declCopy = psiFactory.createDeclaration(decl.getText()!!) + copiedDeclarations[decl] = anchorParent.addBefore(declCopy, anchor) as JetDeclaration + anchorParent.addBefore(psiFactory.createNewLine(), anchor) + } + + val wrappedCall = when (controlFlow) { + is ExpressionEvaluationWithCallSiteReturn -> + psiFactory.createReturn(callText) + + is ParameterUpdate -> + psiFactory.createExpression("${controlFlow.parameter.argumentText} = $callText") + + is Initializer -> { + val newDecl = copiedDeclarations[controlFlow.initializedDeclaration] as JetProperty + newDecl.replace(DeclarationUtils.changePropertyInitializer(newDecl, psiFactory.createExpression(callText))) + null + } + + is ConditionalJump -> + psiFactory.createExpression("if ($callText) ${controlFlow.elementToInsertAfterCall.getText()}") + + is UnconditionalJump -> { + anchorParent.addAfter( + psiFactory.createExpression(controlFlow.elementToInsertAfterCall.getText()!!), + anchor + ) + anchorParent.addAfter(psiFactory.createNewLine(), anchor) + + psiFactory.createExpression(callText) + } + + else -> + psiFactory.createExpression(callText) + } + insertCall(anchor, wrappedCall) + + return function + } + + val function = createFunction() + adjustFunctionBody(function) + + if (options.inTempFile) return ExtractionResult(function, nameByOffset) + + val functionInPlace = makeCall(insertFunction(function)) + ShortenReferences.process(functionInPlace) + return ExtractionResult(functionInPlace, nameByOffset) +} + + diff --git a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java index 2f37e0cf33e..e89dec7bc48 100644 --- a/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java +++ b/idea/src/org/jetbrains/jet/plugin/refactoring/extractFunction/ui/KotlinExtractFunctionDialog.java @@ -54,14 +54,14 @@ public class KotlinExtractFunctionDialog extends DialogWrapper { private final Project project; - private final ExtractionDescriptorWithConflicts originalDescriptor; - private ExtractionDescriptor currentDescriptor; + private final ExtractableCodeDescriptorWithConflicts originalDescriptor; + private ExtractableCodeDescriptor currentDescriptor; private final Function1 onAccept; public KotlinExtractFunctionDialog( @NotNull Project project, - @NotNull ExtractionDescriptorWithConflicts originalDescriptor, + @NotNull ExtractableCodeDescriptorWithConflicts originalDescriptor, @NotNull Function1 onAccept) { super(project, true); @@ -202,8 +202,8 @@ public class KotlinExtractFunctionDialog extends DialogWrapper { } @NotNull - private ExtractionDescriptor createDescriptor() { - ExtractionDescriptor descriptor = originalDescriptor.getDescriptor(); + private ExtractableCodeDescriptor createDescriptor() { + ExtractableCodeDescriptor descriptor = originalDescriptor.getDescriptor(); List parameterInfos = parameterTablePanel.getParameterInfos(); @@ -240,7 +240,7 @@ public class KotlinExtractFunctionDialog extends DialogWrapper { } } - return new ExtractionDescriptor( + return new ExtractableCodeDescriptor( descriptor.getExtractionData(), getFunctionName(), getVisibility(), @@ -253,7 +253,12 @@ public class KotlinExtractFunctionDialog extends DialogWrapper { } @NotNull - public ExtractionDescriptor getCurrentDescriptor() { + public ExtractableCodeDescriptor getCurrentDescriptor() { return currentDescriptor; } + + @NotNull + public ExtractionGeneratorOptions getGeneratorOptions() { + return ExtractionGeneratorOptions.DEFAULT; + } }