Extract Function: Allow to choose between function and property extraction

This commit is contained in:
Alexey Sedunov
2014-08-11 12:45:52 +04:00
parent 87de6cff23
commit 96dae07276
19 changed files with 314 additions and 100 deletions
@@ -25,6 +25,7 @@ import com.intellij.util.LocalTimeCounter
import org.jetbrains.jet.lang.resolve.ImportPath import org.jetbrains.jet.lang.resolve.ImportPath
import org.jetbrains.jet.lexer.JetKeywordToken import org.jetbrains.jet.lexer.JetKeywordToken
import org.jetbrains.jet.plugin.JetFileType import org.jetbrains.jet.plugin.JetFileType
import org.jetbrains.jet.lang.psi.JetPsiFactory.CallableBuilder.Target
public fun JetPsiFactory(project: Project?): JetPsiFactory = JetPsiFactory(project!!) public fun JetPsiFactory(project: Project?): JetPsiFactory = JetPsiFactory(project!!)
public fun JetPsiFactory(contextElement: JetElement): JetPsiFactory = JetPsiFactory(contextElement.getProject()) public fun JetPsiFactory(contextElement: JetElement): JetPsiFactory = JetPsiFactory(contextElement.getProject())
@@ -424,7 +425,12 @@ public class JetPsiFactory(private val project: Project) {
return WhenBuilder(subject?.getText()) return WhenBuilder(subject?.getText())
} }
public class FunctionBuilder() { public class CallableBuilder(private val target: Target) {
public enum class Target {
FUNCTION
READ_ONLY_PROPERTY
}
enum class State { enum class State {
MODIFIERS MODIFIERS
NAME NAME
@@ -440,25 +446,35 @@ public class JetPsiFactory(private val project: Project) {
private var state = State.MODIFIERS private var state = State.MODIFIERS
private fun closeParams() { private fun closeParams() {
assert(state == State.FIRST_PARAM || state == State.REST_PARAMS) if (target == Target.FUNCTION) {
assert(state == State.FIRST_PARAM || state == State.REST_PARAMS)
sb.append(")") sb.append(")")
}
state = State.TYPE_CONSTRAINTS state = State.TYPE_CONSTRAINTS
} }
private fun placeFun() { private fun placeKeyword() {
assert(state == State.MODIFIERS) assert(state == State.MODIFIERS)
if (sb.length() != 0) { if (sb.length() != 0) {
sb.append(" ") sb.append(" ")
} }
sb.append("fun ") val keyword = when (target) {
Target.FUNCTION -> "fun"
Target.READ_ONLY_PROPERTY -> "val"
}
sb.append("$keyword ")
state = State.RECEIVER state = State.RECEIVER
} }
public fun modifier(modifier: String): FunctionBuilder { private fun blockPrefix() = when (target) {
Target.FUNCTION -> ""
Target.READ_ONLY_PROPERTY -> "\nget()"
}
public fun modifier(modifier: String): CallableBuilder {
assert(state == State.MODIFIERS) assert(state == State.MODIFIERS)
sb.append(modifier) sb.append(modifier)
@@ -466,8 +482,8 @@ public class JetPsiFactory(private val project: Project) {
return this return this
} }
public fun typeParams(values: Collection<String>): FunctionBuilder { public fun typeParams(values: Collection<String>): CallableBuilder {
placeFun() placeKeyword()
if (!values.isEmpty()) { if (!values.isEmpty()) {
sb.append(values.joinToString(", ", "<", "> ", -1, "")) sb.append(values.joinToString(", ", "<", "> ", -1, ""))
} }
@@ -475,7 +491,7 @@ public class JetPsiFactory(private val project: Project) {
return this return this
} }
public fun receiver(receiverType: String): FunctionBuilder { public fun receiver(receiverType: String): CallableBuilder {
assert(state == State.RECEIVER) assert(state == State.RECEIVER)
sb.append(receiverType).append(".") sb.append(receiverType).append(".")
@@ -484,16 +500,24 @@ public class JetPsiFactory(private val project: Project) {
return this return this
} }
public fun name(name: String): FunctionBuilder { public fun name(name: String): CallableBuilder {
assert(state == State.NAME || state == State.RECEIVER) assert(state == State.NAME || state == State.RECEIVER)
sb.append(name).append("(") sb.append(name)
state = State.FIRST_PARAM when (target) {
Target.FUNCTION -> {
sb.append("(")
state = State.FIRST_PARAM
}
else ->
state = State.TYPE_CONSTRAINTS
}
return this return this
} }
public fun param(name: String, `type`: String): FunctionBuilder { public fun param(name: String, `type`: String): CallableBuilder {
assert(target == Target.FUNCTION)
assert(state == State.FIRST_PARAM || state == State.REST_PARAMS) assert(state == State.FIRST_PARAM || state == State.REST_PARAMS)
if (state == State.REST_PARAMS) { if (state == State.REST_PARAMS) {
@@ -507,20 +531,20 @@ public class JetPsiFactory(private val project: Project) {
return this return this
} }
public fun returnType(`type`: String): FunctionBuilder { public fun returnType(`type`: String): CallableBuilder {
closeParams() closeParams()
sb.append(": ").append(`type`) sb.append(": ").append(`type`)
return this return this
} }
public fun noReturnType(): FunctionBuilder { public fun noReturnType(): CallableBuilder {
closeParams() closeParams()
return this return this
} }
public fun typeConstraints(values: Collection<String>): FunctionBuilder { public fun typeConstraints(values: Collection<String>): CallableBuilder {
assert(state == State.TYPE_CONSTRAINTS) assert(state == State.TYPE_CONSTRAINTS)
if (!values.isEmpty()) { if (!values.isEmpty()) {
@@ -531,25 +555,25 @@ public class JetPsiFactory(private val project: Project) {
return this return this
} }
public fun simpleBody(body: String): FunctionBuilder { public fun simpleBody(body: String): CallableBuilder {
assert(state == State.BODY || state == State.TYPE_CONSTRAINTS) assert(state == State.BODY || state == State.TYPE_CONSTRAINTS)
sb.append(" = ").append(body) sb.append(blockPrefix()).append(" = ").append(body)
state = State.DONE state = State.DONE
return this return this
} }
public fun blockBody(body: String): FunctionBuilder { public fun blockBody(body: String): CallableBuilder {
assert(state == State.BODY || state == State.TYPE_CONSTRAINTS) assert(state == State.BODY || state == State.TYPE_CONSTRAINTS)
sb.append(" {\n").append(body).append("\n}") sb.append(blockPrefix()).append(" {\n").append(body).append("\n}")
state = State.DONE state = State.DONE
return this return this
} }
public fun toFunctionText(): String { public fun asString(): String {
if (state != State.DONE) { if (state != State.DONE) {
state = State.DONE state = State.DONE
} }
@@ -30,7 +30,9 @@ import org.jetbrains.jet.plugin.refactoring.extractFunction.performAnalysis
import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import org.jetbrains.jet.plugin.refactoring.extractFunction.validate import org.jetbrains.jet.plugin.refactoring.extractFunction.validate
import org.jetbrains.jet.plugin.refactoring.extractFunction.generateFunction import org.jetbrains.jet.lang.psi.JetImportList
import org.jetbrains.jet.lang.psi.JetPsiFactory
import org.jetbrains.jet.lang.psi.JetExpression
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionOptions import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionOptions
import org.jetbrains.jet.plugin.refactoring.runReadAction import org.jetbrains.jet.plugin.refactoring.runReadAction
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
@@ -39,6 +41,7 @@ import org.jetbrains.jet.lang.psi.*
import org.jetbrains.jet.plugin.intentions.InsertExplicitTypeArguments import org.jetbrains.jet.plugin.intentions.InsertExplicitTypeArguments
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionGeneratorOptions import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionGeneratorOptions
import org.jetbrains.jet.plugin.refactoring.extractFunction.generateDeclaration
fun getFunctionForExtractedFragment( fun getFunctionForExtractedFragment(
codeFragment: JetCodeFragment, codeFragment: JetCodeFragment,
@@ -101,8 +104,8 @@ fun getFunctionForExtractedFragment(
} }
return validationResult.descriptor return validationResult.descriptor
.generateFunction(ExtractionGeneratorOptions(inTempFile = true)) .generateDeclaration(ExtractionGeneratorOptions(inTempFile = true))
.function .declaration as JetNamedFunction
} }
return runReadAction { generateFunction() } return runReadAction { generateFunction() }
@@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.psi.*; import org.jetbrains.jet.lang.psi.*;
import org.jetbrains.jet.plugin.refactoring.changeSignature.JetChangeSignatureHandler; import org.jetbrains.jet.plugin.refactoring.changeSignature.JetChangeSignatureHandler;
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractKotlinFunctionHandler; import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractKotlinFunctionHandler;
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractKotlinFunctionHandlerHelper;
import org.jetbrains.jet.plugin.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler; import org.jetbrains.jet.plugin.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler;
import org.jetbrains.jet.plugin.refactoring.safeDelete.SafeDeletePackage; import org.jetbrains.jet.plugin.refactoring.safeDelete.SafeDeletePackage;
@@ -46,7 +47,7 @@ public class JetRefactoringSupportProvider extends RefactoringSupportProvider {
@NotNull @NotNull
public RefactoringActionHandler getExtractFunctionToScopeHandler() { public RefactoringActionHandler getExtractFunctionToScopeHandler() {
return new ExtractKotlinFunctionHandler(true); return new ExtractKotlinFunctionHandler(true, ExtractKotlinFunctionHandlerHelper.DEFAULT);
} }
@Override @Override
@@ -67,13 +67,23 @@ import org.jetbrains.jet.renderer.DescriptorRenderer
import org.jetbrains.jet.lang.psi.JetPropertyAccessor import org.jetbrains.jet.lang.psi.JetPropertyAccessor
import org.jetbrains.jet.lang.psi.JetClassOrObject import org.jetbrains.jet.lang.psi.JetClassOrObject
public class ExtractKotlinFunctionHandler(public val allContainersEnabled: Boolean = false) : RefactoringActionHandler { public open class ExtractKotlinFunctionHandlerHelper {
open fun adjustGeneratorOptions(options: ExtractionGeneratorOptions): ExtractionGeneratorOptions = options
open fun adjustDescriptor(descriptor: ExtractableCodeDescriptor): ExtractableCodeDescriptor = descriptor
class object {
public val DEFAULT: ExtractKotlinFunctionHandlerHelper = ExtractKotlinFunctionHandlerHelper()
}
}
public class ExtractKotlinFunctionHandler(
public val allContainersEnabled: Boolean = false,
private val helper: ExtractKotlinFunctionHandlerHelper = ExtractKotlinFunctionHandlerHelper.DEFAULT) : RefactoringActionHandler {
fun doInvoke( fun doInvoke(
editor: Editor, editor: Editor,
file: JetFile, file: JetFile,
elements: List<PsiElement>, elements: List<PsiElement>,
targetSibling: PsiElement, targetSibling: PsiElement
preprocessor: ((ExtractableCodeDescriptor) -> Unit)? = null
) { ) {
val project = file.getProject() val project = file.getProject()
@@ -84,8 +94,9 @@ public class ExtractKotlinFunctionHandler(public val allContainersEnabled: Boole
} }
fun doRefactor(descriptor: ExtractableCodeDescriptor, generatorOptions: ExtractionGeneratorOptions) { fun doRefactor(descriptor: ExtractableCodeDescriptor, generatorOptions: ExtractionGeneratorOptions) {
preprocessor?.invoke(descriptor) val adjustedDescriptor = helper.adjustDescriptor(descriptor)
project.executeWriteCommand(EXTRACT_FUNCTION) { descriptor.generateFunction(generatorOptions) } val adjustedGeneratorOptions = helper.adjustGeneratorOptions(generatorOptions)
project.executeWriteCommand(EXTRACT_FUNCTION) { adjustedDescriptor.generateDeclaration(adjustedGeneratorOptions) }
} }
fun validateAndRefactor() { fun validateAndRefactor() {
@@ -38,7 +38,9 @@ import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor
import org.jetbrains.jet.lang.psi.JetProperty import org.jetbrains.jet.lang.psi.JetProperty
import org.jetbrains.jet.lang.psi.JetDeclaration import org.jetbrains.jet.lang.psi.JetDeclaration
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.jet.lang.psi.JetNamedFunction import org.jetbrains.jet.lang.psi.JetClassBody
import org.jetbrains.jet.lang.psi.JetFile
import org.jetbrains.jet.lang.psi.JetNamedDeclaration
trait Parameter { trait Parameter {
val argumentText: String val argumentText: String
@@ -164,8 +166,9 @@ data class ExtractableCodeDescriptor(
val controlFlow: ControlFlow val controlFlow: ControlFlow
) )
class ExtractionGeneratorOptions( data class ExtractionGeneratorOptions(
val inTempFile: Boolean = false val inTempFile: Boolean = false,
val extractAsProperty: Boolean = false
) { ) {
class object { class object {
val DEFAULT = ExtractionGeneratorOptions() val DEFAULT = ExtractionGeneratorOptions()
@@ -173,7 +176,7 @@ class ExtractionGeneratorOptions(
} }
data class ExtractionResult( data class ExtractionResult(
val function: JetNamedFunction, val declaration: JetNamedDeclaration,
val nameByOffset: Map<Int, JetElement> val nameByOffset: Map<Int, JetElement>
) )
@@ -232,3 +235,10 @@ class ExtractableCodeDescriptorWithConflicts(
val descriptor: ExtractableCodeDescriptor, val descriptor: ExtractableCodeDescriptor,
val conflicts: MultiMap<PsiElement, String> val conflicts: MultiMap<PsiElement, String>
) )
fun ExtractableCodeDescriptor.canGenerateProperty(): Boolean {
if (!parameters.empty) return false
val parent = extractionData.targetSibling.getParent()
return parent is JetFile || parent is JetClassBody
}
@@ -274,7 +274,7 @@ private fun ExtractionData.analyzeControlFlow(
return Pair(defaultControlFlow, null) return Pair(defaultControlFlow, null)
} }
fun ExtractionData.createTemporaryFunction(functionText: String): JetNamedFunction { fun ExtractionData.createTemporaryDeclaration(functionText: String): JetNamedDeclaration {
val textRange = targetSibling.getTextRange()!! val textRange = targetSibling.getTextRange()!!
val insertText: String val insertText: String
@@ -294,11 +294,11 @@ fun ExtractionData.createTemporaryFunction(functionText: String): JetNamedFuncti
val tmpFile = originalFile.createTempCopy { text -> val tmpFile = originalFile.createTempCopy { text ->
StringBuilder(text).insert(insertPosition, insertText).toString() StringBuilder(text).insert(insertPosition, insertText).toString()
} }
return tmpFile.findElementAt(lookupPosition)?.getParentByType(javaClass<JetNamedFunction>())!! return tmpFile.findElementAt(lookupPosition)?.getParentByType(javaClass<JetNamedDeclaration>())!!
} }
private fun ExtractionData.createTemporaryCodeBlock(): JetBlockExpression = private fun ExtractionData.createTemporaryCodeBlock(): JetBlockExpression =
createTemporaryFunction("fun() {\n${getCodeFragmentText()}\n}\n").getBodyExpression() as JetBlockExpression (createTemporaryDeclaration("fun() {\n${getCodeFragmentText()}\n}\n") as JetNamedFunction).getBodyExpression() as JetBlockExpression
private fun JetType.collectReferencedTypes(processTypeArguments: Boolean): List<JetType> { private fun JetType.collectReferencedTypes(processTypeArguments: Boolean): List<JetType> {
if (!processTypeArguments) return Collections.singletonList(this) if (!processTypeArguments) return Collections.singletonList(this)
@@ -683,12 +683,19 @@ fun ExtractionData.performAnalysis(): AnalysisResult {
) )
} }
private fun JetNamedDeclaration.getGeneratedBlockBody() =
when (this) {
is JetNamedFunction -> getBodyExpression()
else -> (this as JetProperty).getGetter()!!.getBodyExpression()
} as JetBlockExpression
fun ExtractableCodeDescriptor.validate(): ExtractableCodeDescriptorWithConflicts { fun ExtractableCodeDescriptor.validate(): ExtractableCodeDescriptorWithConflicts {
val conflicts = MultiMap<PsiElement, String>() val conflicts = MultiMap<PsiElement, String>()
val result = generateFunction(ExtractionGeneratorOptions(inTempFile = true)) val result = generateDeclaration(ExtractionGeneratorOptions(inTempFile = true))
val bindingContext = AnalyzerFacadeWithCache.getContextForElement(result.function.getBodyExpression()!!) val valueParameterList = (result.declaration as? JetNamedFunction)?.getValueParameterList()
val bindingContext = AnalyzerFacadeWithCache.getContextForElement(result.declaration.getGeneratedBlockBody())
for ((originalOffset, resolveResult) in extractionData.refOffsetToDeclaration) { for ((originalOffset, resolveResult) in extractionData.refOffsetToDeclaration) {
if (resolveResult.declaration.isInsideOf(extractionData.originalElements)) continue if (resolveResult.declaration.isInsideOf(extractionData.originalElements)) continue
@@ -703,7 +710,7 @@ fun ExtractableCodeDescriptor.validate(): ExtractableCodeDescriptorWithConflicts
val currentDescriptor = bindingContext[BindingContext.REFERENCE_TARGET, currentRefExpr] val currentDescriptor = bindingContext[BindingContext.REFERENCE_TARGET, currentRefExpr]
val currentTarget = val currentTarget =
currentDescriptor?.let { DescriptorToDeclarationUtil.getDeclaration(extractionData.project, it) } as? PsiNamedElement currentDescriptor?.let { DescriptorToDeclarationUtil.getDeclaration(extractionData.project, it) } as? PsiNamedElement
if (currentTarget is JetParameter && currentTarget.getParent() == result.function.getValueParameterList()) continue if (currentTarget is JetParameter && currentTarget.getParent() == valueParameterList) continue
if (currentDescriptor is LocalVariableDescriptor if (currentDescriptor is LocalVariableDescriptor
&& parameters.any { it.mirrorVarName == currentDescriptor.getName().asString() }) continue && parameters.any { it.mirrorVarName == currentDescriptor.getName().asString() }) continue
@@ -17,39 +17,44 @@
package org.jetbrains.jet.plugin.refactoring.extractFunction package org.jetbrains.jet.plugin.refactoring.extractFunction
import org.jetbrains.jet.renderer.DescriptorRenderer 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.JetElement
import org.jetbrains.jet.lang.psi.JetSimpleNameExpression import org.jetbrains.jet.lang.psi.JetSimpleNameExpression
import java.util.HashMap import java.util.HashMap
import org.jetbrains.jet.lang.psi.JetTreeVisitorVoid import org.jetbrains.jet.lang.psi.JetTreeVisitorVoid
import org.jetbrains.jet.lang.psi.psiUtil.getParentByType import org.jetbrains.jet.lang.psi.psiUtil.getParentByType
import org.jetbrains.jet.lang.psi.JetNamedFunction 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 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 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.JetDeclaration
import org.jetbrains.jet.lang.psi.JetProperty import org.jetbrains.jet.lang.psi.JetProperty
import org.jetbrains.jet.plugin.intentions.declarations.DeclarationUtils import org.jetbrains.jet.lang.psi.JetPsiFactory.CallableBuilder
import org.jetbrains.jet.lang.psi.JetPsiFactory.CallableBuilder.Target
import org.jetbrains.jet.lang.psi.JetNamedDeclaration
import org.jetbrains.jet.lang.psi.JetPsiFactory
import java.util.LinkedHashMap
import java.util.Collections
import org.jetbrains.jet.plugin.codeInsight.ShortenReferences import org.jetbrains.jet.plugin.codeInsight.ShortenReferences
import org.jetbrains.jet.lang.psi.psiUtil.isFunctionLiteralOutsideParentheses import org.jetbrains.jet.lang.psi.psiUtil.isFunctionLiteralOutsideParentheses
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.jet.lang.psi.JetFunctionLiteralArgument import org.jetbrains.jet.lang.psi.JetFunctionLiteralArgument
import org.jetbrains.jet.lang.resolve.BindingContext import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.plugin.util.psiModificationUtil.moveInsideParenthesesAndReplaceWith import org.jetbrains.jet.plugin.util.psiModificationUtil.moveInsideParenthesesAndReplaceWith
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 org.jetbrains.jet.plugin.intentions.declarations.DeclarationUtils
fun ExtractableCodeDescriptor.getFunctionText( fun ExtractableCodeDescriptor.getDeclarationText(
options: ExtractionGeneratorOptions = ExtractionGeneratorOptions.DEFAULT,
withBody: Boolean = true, withBody: Boolean = true,
descriptorRenderer: DescriptorRenderer = DescriptorRenderer.FQ_NAMES_IN_TYPES descriptorRenderer: DescriptorRenderer = DescriptorRenderer.FQ_NAMES_IN_TYPES
): String { ): String {
return FunctionBuilder().let { builder -> if (!canGenerateProperty() && options.extractAsProperty) {
throw IllegalArgumentException("Can't generate property: ${extractionData.getCodeFragmentText()}")
}
val builderTarget = if (options.extractAsProperty) Target.READ_ONLY_PROPERTY else Target.FUNCTION
return CallableBuilder(builderTarget).let { builder ->
builder.modifier(visibility) builder.modifier(visibility)
builder.typeParams(typeParameters.map { it.originalDeclaration.getText()!! }) builder.typeParams(typeParameters.map { it.originalDeclaration.getText()!! })
@@ -72,7 +77,7 @@ fun ExtractableCodeDescriptor.getFunctionText(
builder.blockBody(extractionData.getCodeFragmentText()) builder.blockBody(extractionData.getCodeFragmentText())
} }
builder.toFunctionText() builder.asString()
} }
} }
@@ -95,23 +100,23 @@ fun createNameCounterpartMap(from: JetElement, to: JetElement): Map<JetSimpleNam
return map return map
} }
fun ExtractableCodeDescriptor.generateFunction(options: ExtractionGeneratorOptions): ExtractionResult { fun ExtractableCodeDescriptor.generateDeclaration(options: ExtractionGeneratorOptions): ExtractionResult{
val psiFactory = JetPsiFactory(extractionData.originalFile) val psiFactory = JetPsiFactory(extractionData.originalFile)
val nameByOffset = HashMap<Int, JetElement>() val nameByOffset = HashMap<Int, JetElement>()
fun createFunction(): JetNamedFunction { fun createDeclaration(): JetNamedDeclaration {
return with(extractionData) { return with(extractionData) {
if (options.inTempFile) { if (options.inTempFile) {
createTemporaryFunction("${getFunctionText()}\n") createTemporaryDeclaration("${getDeclarationText()}\n")
} }
else { else {
psiFactory.createFunction(getFunctionText()) psiFactory.createDeclaration(getDeclarationText(options))
} }
} }
} }
fun adjustFunctionBody(function: JetNamedFunction) { fun adjustDeclarationBody(declaration: JetNamedDeclaration) {
val body = function.getBodyExpression() as JetBlockExpression val body = declaration.getGeneratedBlockBody()
val exprReplacementMap = HashMap<JetElement, (JetElement) -> JetElement>() val exprReplacementMap = HashMap<JetElement, (JetElement) -> JetElement>()
val originalOffsetByExpr = LinkedHashMap<JetElement, Int>() val originalOffsetByExpr = LinkedHashMap<JetElement, Int>()
@@ -183,28 +188,32 @@ fun ExtractableCodeDescriptor.generateFunction(options: ExtractionGeneratorOptio
is ExpressionEvaluation -> is ExpressionEvaluation ->
body.getStatements().last?.let { 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 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) val counterpartMap = createNameCounterpartMap(it, newExpr)
nameByOffset.entrySet().forEach { e -> counterpartMap[e.getValue()]?.let { e.setValue(it) } } nameByOffset.entrySet().forEach { e -> counterpartMap[e.getValue()]?.let { e.setValue(it) } }
} }
} }
} }
fun insertFunction(function: JetNamedFunction): JetNamedFunction { fun insertDeclaration(declaration: JetNamedDeclaration): JetNamedDeclaration {
return with(extractionData) { return with(extractionData) {
val targetContainer = targetSibling.getParent()!! val targetContainer = targetSibling.getParent()!!
val emptyLines = psiFactory.createWhiteSpace("\n\n") val emptyLines = psiFactory.createWhiteSpace("\n\n")
if (insertBefore) { if (insertBefore) {
val functionInFile = targetContainer.addBefore(function, targetSibling) as JetNamedFunction val declarationInFile = targetContainer.addBefore(declaration, targetSibling) as JetNamedDeclaration
targetContainer.addBefore(emptyLines, targetSibling) targetContainer.addBefore(emptyLines, targetSibling)
functionInFile declarationInFile
} }
else { else {
val functionInFile = targetContainer.addAfter(function, targetSibling) as JetNamedFunction val declarationInFile = targetContainer.addAfter(declaration, targetSibling) as JetNamedDeclaration
targetContainer.addAfter(emptyLines, targetSibling) targetContainer.addAfter(emptyLines, targetSibling)
functionInFile declarationInFile
} }
} }
} }
@@ -225,9 +234,9 @@ fun ExtractableCodeDescriptor.generateFunction(options: ExtractionGeneratorOptio
anchor.replace(wrappedCall) anchor.replace(wrappedCall)
} }
fun makeCall(function: JetNamedFunction): JetNamedFunction { fun makeCall(declaration: JetNamedDeclaration): JetNamedDeclaration {
val anchor = extractionData.originalElements.first val anchor = extractionData.originalElements.first
if (anchor == null) return function if (anchor == null) return declaration
val anchorParent = anchor.getParent()!! val anchorParent = anchor.getParent()!!
@@ -238,9 +247,13 @@ fun ExtractableCodeDescriptor.generateFunction(options: ExtractionGeneratorOptio
} }
} }
val callText = parameters val callText = when (declaration) {
.map { it.argumentText } is JetNamedFunction ->
.joinToString(separator = ", ", prefix = "${name}(", postfix = ")") parameters
.map { it.argumentText }
.joinToString(separator = ", ", prefix = "${name}(", postfix = ")")
else -> name
}
val copiedDeclarations = HashMap<JetDeclaration, JetDeclaration>() val copiedDeclarations = HashMap<JetDeclaration, JetDeclaration>()
for (decl in controlFlow.declarationsToCopy) { for (decl in controlFlow.declarationsToCopy) {
@@ -280,17 +293,15 @@ fun ExtractableCodeDescriptor.generateFunction(options: ExtractionGeneratorOptio
} }
insertCall(anchor, wrappedCall) insertCall(anchor, wrappedCall)
return function return declaration
} }
val function = createFunction() val declaration = createDeclaration()
adjustFunctionBody(function) adjustDeclarationBody(declaration)
if (options.inTempFile) return ExtractionResult(function, nameByOffset) if (options.inTempFile) return ExtractionResult(declaration, nameByOffset)
val functionInPlace = makeCall(insertFunction(function)) val declarationInPlace = makeCall(insertDeclaration(declaration))
ShortenReferences.process(functionInPlace) ShortenReferences.process(declarationInPlace)
return ExtractionResult(functionInPlace, nameByOffset) return ExtractionResult(declaration, nameByOffset)
} }
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.jet.plugin.refactoring.extractFunction.ui.KotlinExtractFunctionDialog"> <form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.jet.plugin.refactoring.extractFunction.ui.KotlinExtractFunctionDialog">
<grid id="27dc6" binding="contentPane" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <grid id="27dc6" binding="contentPane" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/> <margin top="0" left="0" bottom="0" right="0"/>
<constraints> <constraints>
<xy x="20" y="20" width="522" height="318"/> <xy x="20" y="20" width="522" height="351"/>
</constraints> </constraints>
<properties/> <properties/>
<border type="none"/> <border type="none"/>
@@ -11,7 +11,7 @@
<grid id="34b30" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <grid id="34b30" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/> <margin top="0" left="0" bottom="0" right="0"/>
<constraints> <constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/> <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints> </constraints>
<properties/> <properties/>
<border type="none"/> <border type="none"/>
@@ -38,7 +38,7 @@
<grid id="b45d1" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1"> <grid id="b45d1" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/> <margin top="0" left="0" bottom="0" right="0"/>
<constraints> <constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"> <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
<preferred-size width="-1" height="200"/> <preferred-size width="-1" height="200"/>
</grid> </grid>
</constraints> </constraints>
@@ -127,9 +127,32 @@
</grid> </grid>
<vspacer id="cd10f"> <vspacer id="cd10f">
<constraints> <constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/> <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints> </constraints>
</vspacer> </vspacer>
<grid id="ad40b" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="1b96b" class="javax.swing.JCheckBox" binding="propertyCheckBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Extract as property"/>
</properties>
</component>
<hspacer id="df17e">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
</children>
</grid>
</children> </children>
</grid> </grid>
</form> </form>
@@ -36,6 +36,8 @@ import org.jetbrains.jet.plugin.refactoring.extractFunction.*;
import org.jetbrains.jet.renderer.DescriptorRenderer; import org.jetbrains.jet.renderer.DescriptorRenderer;
import javax.swing.*; import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent; import java.awt.event.ItemEvent;
import java.awt.event.ItemListener; import java.awt.event.ItemListener;
import java.util.List; import java.util.List;
@@ -48,6 +50,7 @@ public class KotlinExtractFunctionDialog extends DialogWrapper {
private KotlinFunctionSignatureComponent signaturePreviewField; private KotlinFunctionSignatureComponent signaturePreviewField;
private EditorTextField functionNameField; private EditorTextField functionNameField;
private JLabel functionNameLabel; private JLabel functionNameLabel;
private JCheckBox propertyCheckBox;
private KotlinParameterTablePanel parameterTablePanel; private KotlinParameterTablePanel parameterTablePanel;
private final Project project; private final Project project;
@@ -106,7 +109,8 @@ public class KotlinExtractFunctionDialog extends DialogWrapper {
setOKActionEnabled(checkNames()); setOKActionEnabled(checkNames());
signaturePreviewField.setText( signaturePreviewField.setText(
ExtractFunctionPackage.getFunctionText(currentDescriptor, false, DescriptorRenderer.SOURCE_CODE_SHORT_NAMES_IN_TYPES) ExtractFunctionPackage.getDeclarationText(currentDescriptor, getGeneratorOptions(), false,
DescriptorRenderer.SOURCE_CODE_SHORT_NAMES_IN_TYPES)
); );
} }
@@ -140,6 +144,18 @@ public class KotlinExtractFunctionDialog extends DialogWrapper {
} }
); );
propertyCheckBox.setEnabled(ExtractFunctionPackage.canGenerateProperty(originalDescriptor.getDescriptor()));
if (propertyCheckBox.isEnabled()) {
propertyCheckBox.addActionListener(
new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
update();
}
}
);
}
parameterTablePanel = new KotlinParameterTablePanel() { parameterTablePanel = new KotlinParameterTablePanel() {
@Override @Override
protected void updateSignature() { protected void updateSignature() {
@@ -256,6 +272,6 @@ public class KotlinExtractFunctionDialog extends DialogWrapper {
@NotNull @NotNull
public ExtractionGeneratorOptions getGeneratorOptions() { public ExtractionGeneratorOptions getGeneratorOptions() {
return ExtractionGeneratorOptions.DEFAULT; return new ExtractionGeneratorOptions(false, propertyCheckBox.isSelected());
} }
} }
@@ -0,0 +1,10 @@
// EXTRACT_AS_PROPERTY
class A(val n: Int = 1) {
val m: Int = 2
// SIBLING:
fun foo(): Int {
return <selection>m + n + 1</selection>
}
}
@@ -0,0 +1,15 @@
// EXTRACT_AS_PROPERTY
class A(val n: Int = 1) {
val m: Int = 2
// SIBLING:
fun foo(): Int {
return i
}
private val i: Int
get() {
return m + n + 1
}
}
@@ -0,0 +1,8 @@
// EXTRACT_AS_PROPERTY
val n: Int = 1
// SIBLING:
fun foo(): Int {
return <selection>n + 1</selection>
}
@@ -0,0 +1,13 @@
// EXTRACT_AS_PROPERTY
val n: Int = 1
// SIBLING:
fun foo(): Int {
return i
}
private val i: Int
get() {
return n + 1
}
@@ -0,0 +1,6 @@
// EXTRACT_AS_PROPERTY
fun foo(n: Int): Int {
// SIBLING:
return {<selection>n + 1</selection>}()
}
@@ -0,0 +1 @@
Can't generate property: n + 1
@@ -0,0 +1,11 @@
// PARAM_TYPES: kotlin.Int
// PARAM_DESCRIPTOR: val m: kotlin.Int defined in foo
// EXTRACT_AS_PROPERTY
val n: Int = 1
// SIBLING:
fun foo(): Int {
val m = 1
return <selection>n + m + 1</selection>
}
@@ -0,0 +1 @@
Can't generate property: n + m + 1
@@ -38,6 +38,9 @@ import com.intellij.util.containers.ContainerUtil
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.jetbrains.jet.plugin.JetLightCodeInsightFixtureTestCase import org.jetbrains.jet.plugin.JetLightCodeInsightFixtureTestCase
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractKotlinFunctionHandlerHelper
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractionGeneratorOptions
import org.jetbrains.jet.plugin.refactoring.extractFunction.ExtractableCodeDescriptor
public abstract class AbstractJetExtractionTest() : JetLightCodeInsightFixtureTestCase() { public abstract class AbstractJetExtractionTest() : JetLightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor() = LightCodeInsightFixtureTestCase.JAVA_LATEST override fun getProjectDescriptor() = LightCodeInsightFixtureTestCase.JAVA_LATEST
@@ -78,21 +81,32 @@ public abstract class AbstractJetExtractionTest() : JetLightCodeInsightFixtureTe
InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PARAM_DESCRIPTOR: ").joinToString() InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PARAM_DESCRIPTOR: ").joinToString()
val expectedTypes = val expectedTypes =
InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PARAM_TYPES: ").map { "[$it]" }.joinToString() InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PARAM_TYPES: ").map { "[$it]" }.joinToString()
val extractAsProperty = InTextDirectivesUtils.isDirectiveDefined(fileText, "// EXTRACT_AS_PROPERTY")
val renderer = DescriptorRenderer.DEBUG_TEXT val renderer = DescriptorRenderer.DEBUG_TEXT
val editor = fixture.getEditor() val editor = fixture.getEditor()
selectElements(editor, file) { (elements, previousSibling) -> selectElements(editor, file) {(elements, previousSibling) ->
ExtractKotlinFunctionHandler().doInvoke(editor, file, elements, explicitPreviousSibling ?: previousSibling) { ExtractKotlinFunctionHandler(
val allParameters = ContainerUtil.createMaybeSingletonList(it.receiverParameter) + it.parameters helper = object : ExtractKotlinFunctionHandlerHelper() {
val actualDescriptors = allParameters.map { renderer.render(it.originalDescriptor) }.joinToString() override fun adjustGeneratorOptions(options: ExtractionGeneratorOptions): ExtractionGeneratorOptions {
val actualTypes = allParameters.map { return options.copy(extractAsProperty = extractAsProperty)
it.parameterTypeCandidates.map { renderer.renderType(it) }.joinToString(", ", "[", "]") }
}.joinToString()
assertEquals(expectedDescriptors, actualDescriptors, "Expected descriptors mismatch.") override fun adjustDescriptor(descriptor: ExtractableCodeDescriptor): ExtractableCodeDescriptor {
assertEquals(expectedTypes, actualTypes, "Expected types mismatch.") val allParameters = ContainerUtil.createMaybeSingletonList(descriptor.receiverParameter) + descriptor.parameters
} val actualDescriptors = allParameters.map { renderer.render(it.originalDescriptor) }.joinToString()
val actualTypes = allParameters.map {
it.parameterTypeCandidates.map { renderer.renderType(it) }.joinToString(", ", "[", "]")
}.joinToString()
assertEquals(expectedDescriptors, actualDescriptors, "Expected descriptors mismatch.")
assertEquals(expectedTypes, actualTypes, "Expected types mismatch.")
return descriptor
}
}
).doInvoke(editor, file, elements, explicitPreviousSibling ?: previousSibling)
} }
} }
} }
@@ -211,12 +211,40 @@ public class JetExtractionTestGenerated extends AbstractJetExtractionTest {
} }
@TestMetadata("idea/testData/refactoring/extractFunction") @TestMetadata("idea/testData/refactoring/extractFunction")
@InnerTestClasses({ExtractFunction.Basic.class, ExtractFunction.ControlFlow.class, ExtractFunction.DefaultContainer.class, ExtractFunction.Delegation.class, ExtractFunction.Initializers.class, ExtractFunction.Parameters.class, ExtractFunction.TypeParameters.class}) @InnerTestClasses({ExtractFunction.AsProperty.class, ExtractFunction.Basic.class, ExtractFunction.ControlFlow.class, ExtractFunction.DefaultContainer.class, ExtractFunction.Delegation.class, ExtractFunction.Initializers.class, ExtractFunction.Parameters.class, ExtractFunction.TypeParameters.class})
public static class ExtractFunction extends AbstractJetExtractionTest { public static class ExtractFunction extends AbstractJetExtractionTest {
public void testAllFilesPresentInExtractFunction() throws Exception { public void testAllFilesPresentInExtractFunction() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.TestsPackage", new File("idea/testData/refactoring/extractFunction"), Pattern.compile("^(.+)\\.kt$"), true); JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.TestsPackage", new File("idea/testData/refactoring/extractFunction"), Pattern.compile("^(.+)\\.kt$"), true);
} }
@TestMetadata("idea/testData/refactoring/extractFunction/asProperty")
public static class AsProperty extends AbstractJetExtractionTest {
public void testAllFilesPresentInAsProperty() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.TestsPackage", new File("idea/testData/refactoring/extractFunction/asProperty"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("extractToClass.kt")
public void testExtractToClass() throws Exception {
doExtractFunctionTest("idea/testData/refactoring/extractFunction/asProperty/extractToClass.kt");
}
@TestMetadata("extractToFile.kt")
public void testExtractToFile() throws Exception {
doExtractFunctionTest("idea/testData/refactoring/extractFunction/asProperty/extractToFile.kt");
}
@TestMetadata("extractToFunction.kt")
public void testExtractToFunction() throws Exception {
doExtractFunctionTest("idea/testData/refactoring/extractFunction/asProperty/extractToFunction.kt");
}
@TestMetadata("extractWithParams.kt")
public void testExtractWithParams() throws Exception {
doExtractFunctionTest("idea/testData/refactoring/extractFunction/asProperty/extractWithParams.kt");
}
}
@TestMetadata("idea/testData/refactoring/extractFunction/basic") @TestMetadata("idea/testData/refactoring/extractFunction/basic")
public static class Basic extends AbstractJetExtractionTest { public static class Basic extends AbstractJetExtractionTest {
public void testAllFilesPresentInBasic() throws Exception { public void testAllFilesPresentInBasic() throws Exception {
@@ -1311,6 +1339,7 @@ public class JetExtractionTestGenerated extends AbstractJetExtractionTest {
public static Test innerSuite() { public static Test innerSuite() {
TestSuite suite = new TestSuite("ExtractFunction"); TestSuite suite = new TestSuite("ExtractFunction");
suite.addTestSuite(ExtractFunction.class); suite.addTestSuite(ExtractFunction.class);
suite.addTestSuite(AsProperty.class);
suite.addTestSuite(Basic.class); suite.addTestSuite(Basic.class);
suite.addTest(ControlFlow.innerSuite()); suite.addTest(ControlFlow.innerSuite());
suite.addTestSuite(DefaultContainer.class); suite.addTestSuite(DefaultContainer.class);