Extract Function: Analysis and code generation

This commit is contained in:
Alexey Sedunov
2014-03-25 19:08:53 +04:00
parent cceaa3b66f
commit 424c5f296f
10 changed files with 1066 additions and 14 deletions
@@ -67,12 +67,14 @@ public class JetControlFlowProcessor {
this.trace = trace;
}
@NotNull
public Pseudocode generatePseudocode(@NotNull JetElement subroutine) {
Pseudocode pseudocode = generate(subroutine);
((PseudocodeImpl) pseudocode).postProcess();
return pseudocode;
}
@NotNull
private Pseudocode generate(@NotNull JetElement subroutine) {
builder.enterSubroutine(subroutine);
CFPVisitor cfpVisitor = new CFPVisitor(builder);
@@ -32,7 +32,7 @@ import org.jetbrains.jet.util.slicedmap.WritableSlice;
import java.util.Collection;
public class PseudocodeUtil {
@NotNull
public static Pseudocode generatePseudocode(@NotNull JetDeclaration declaration, @NotNull final BindingContext bindingContext) {
BindingTrace mockTrace = new BindingTrace() {
@NotNull
@@ -28,7 +28,6 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.resolve.ImportPath;
import org.jetbrains.jet.lang.resolve.name.Name;
import org.jetbrains.jet.lexer.JetKeywordToken;
import org.jetbrains.jet.lexer.JetTokens;
import org.jetbrains.jet.plugin.JetFileType;
import java.util.Collections;
@@ -132,7 +131,7 @@ public class JetPsiFactory {
}
@NotNull
private static PsiElement createWhiteSpace(Project project, String text) {
public static PsiElement createWhiteSpace(Project project, String text) {
JetProperty property = createProperty(project, "val" + text + "x");
return property.findElementAt(3);
}
@@ -523,6 +522,135 @@ public class JetPsiFactory {
}
}
public static class FunctionBuilder {
static enum State {
MODIFIERS,
NAME,
FIRST_PARAM,
REST_PARAMS,
BODY,
DONE
}
private final StringBuilder sb = new StringBuilder();
private State state = State.MODIFIERS;
public FunctionBuilder() {
}
private void closeParams() {
assert state == State.FIRST_PARAM || state == State.REST_PARAMS;
sb.append(")");
state = State.BODY;
}
private void placeFun() {
assert state == State.MODIFIERS;
if (sb.length() != 0) {
sb.append(" ");
}
sb.append("fun ");
state = State.NAME;
}
@NotNull
public FunctionBuilder modifier(@NotNull String modifier) {
assert state == State.MODIFIERS;
sb.append(modifier);
return this;
}
@NotNull
public FunctionBuilder receiver(@NotNull String receiverType) {
placeFun();
sb.append(receiverType).append(".");
return this;
}
@NotNull
public FunctionBuilder noReceiver() {
placeFun();
return this;
}
@NotNull
public FunctionBuilder name(@NotNull String name) {
assert state == State.NAME;
sb.append(name).append("(");
state = State.FIRST_PARAM;
return this;
}
@NotNull
public FunctionBuilder param(@NotNull String name, @NotNull String type) {
assert state == State.FIRST_PARAM || state == State.REST_PARAMS;
if (state == State.REST_PARAMS) {
sb.append(", ");
}
sb.append(name).append(": ").append(type);
if (state == State.FIRST_PARAM) {
state = State.REST_PARAMS;
}
return this;
}
@NotNull
public FunctionBuilder returnType(@NotNull String type) {
closeParams();
sb.append(": ").append(type);
return this;
}
@NotNull
public FunctionBuilder noReturnType() {
closeParams();
return this;
}
@NotNull
public FunctionBuilder simpleBody(@NotNull String body) {
assert state == State.BODY;
sb.append(" = ").append(body);
state = State.DONE;
return this;
}
@NotNull
public FunctionBuilder blockBody(@NotNull String body) {
assert state == State.BODY;
sb.append(" {\n").append(body).append("\n}");
state = State.DONE;
return this;
}
@NotNull
public String toFunctionText() {
if (state != State.DONE) {
state = State.DONE;
}
return sb.toString();
}
}
@NotNull
public static JetExpression createFunctionBody(Project project, @NotNull String bodyText) {
JetFunction func = createFunction(project, "fun foo() {\n" + bodyText + "\n}");
@@ -104,6 +104,9 @@ public fun JetElement.outermostLastBlockElement(predicate: (JetElement) -> Boole
public fun JetBlockExpression.appendElement(element: JetElement): JetElement =
addAfter(element, getRBrace()!!.getPrevSibling()!!)!! as JetElement
public fun JetBlockExpression.prependElement(element: JetElement): JetElement =
addBefore(element, getLBrace()!!.getNextSibling()!!)!! as JetElement
public fun JetElement.wrapInBlock(): JetBlockExpression {
val block = JetPsiFactory.createEmptyBody(getProject()) as JetBlockExpression
block.appendElement(this)
@@ -256,4 +259,9 @@ public fun PsiDirectory.getPackage(): PsiPackage? = JavaDirectoryService.getInst
public fun JetModifierListOwner.isPrivate(): Boolean = hasModifier(JetTokens.PRIVATE_KEYWORD)
public fun PsiElement.isInsideOf(elements: Iterable<PsiElement>): Boolean = elements.any { it.isAncestor(this) }
public fun PsiElement.isInsideOf(elements: Iterable<PsiElement>): Boolean = elements.any { it.isAncestor(this) }
public tailRecursive fun PsiElement.getOutermostParentContainedIn(container: PsiElement): PsiElement? {
val parent = getParent()
return if (parent == container) this else parent?.getOutermostParentContainedIn(container)
}
@@ -25,6 +25,7 @@ import org.jetbrains.jet.lang.descriptors.ClassDescriptor
import org.jetbrains.jet.lang.descriptors.CallableDescriptor
import org.jetbrains.jet.lang.psi.JetSimpleNameExpression
import com.intellij.psi.PsiElement
import org.jetbrains.jet.lang.types.JetType
public val DeclarationDescriptor.importableFqName: FqName?
get() {
@@ -53,6 +54,11 @@ public fun DeclarationDescriptor.canBeReferencedViaImport(): Boolean {
return this is ClassDescriptor || this is ConstructorDescriptor
}
public fun JetType.canBeReferencedViaImport(): Boolean {
val descriptor = getConstructor().getDeclarationDescriptor()
return descriptor != null && descriptor.canBeReferencedViaImport()
}
public fun isInReceiverScope(referenceElement: PsiElement, referencedDescriptor: DeclarationDescriptor): Boolean {
val isExpressionWithReceiver = referenceElement is JetSimpleNameExpression && referenceElement.getReceiverExpression() != null
return isExpressionWithReceiver && !referencedDescriptor.isExtension
@@ -6,8 +6,17 @@ cannot.refactor.no.container=Cannot refactor in this place
cannot.refactor.no.expression=Cannot perform refactoring without an expression
cannot.refactor.expression.has.unit.type=Cannot introduce expression of unit type
cannot.refactor.package.expression=Cannot introduce package reference
extract.function=Extract Function
cannot.extract.method=Cannot find statements to extract
cannot.find.class.to.extract=Cannot find class to extract method
selected.code.fragment.has.multiple.output.values=Selected code fragment has multiple output values:
variables.are.used.outside.of.selected.code.fragment=Following variables are used outside of selected code fragment:
selected.code.fragment.has.multiple.exit.points=Selected code fragment has multiple exit points
selected.code.fragment.has.output.values.and.exit.points=Selected code fragment has output values as well as alternative exit points
parameter.types.are.not.denotable=Cannot extract method since following types are not denotable in the target scope:
declarations.will.move.out.of.scope=Following declarations won't be available outside of extracted function body:
cannot.extract.super.call=Cannot extract super-call
cannot.extract.non.local.declaration.ref=Cannot extract reference to non-local declaration
cannot.refactor.expression.should.have.inferred.type=Expression should have inferred type
cannot.refactor.synthesized.function=Cannot refactor synthesized function ''{0}''
@@ -26,4 +35,5 @@ refactoring.move.top.level.declaration.file.title=Choose Destination File
refactoring.move.non.kotlin.file=Target must be a Kotlin file
package.private.0.will.no.longer.be.accessible.from.1=Package-private {0} will no longer be accessible from {1}
0.uses.package.private.1={0} uses package-private {1}
0.uses.package.private.1={0} uses package-private {1}
0.will.no.longer.be.accessible.after.extraction={0} will no longer be accessible after extraction
@@ -0,0 +1,139 @@
/*
* 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 com.intellij.psi.PsiElement
import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor
import org.jetbrains.jet.lang.resolve.calls.model.ResolvedCall
import org.jetbrains.jet.lang.psi.JetFile
import com.intellij.openapi.project.Project
import org.jetbrains.jet.lang.psi.JetExpression
import com.intellij.openapi.util.TextRange
import kotlin.properties.Delegates
import java.util.HashMap
import org.jetbrains.jet.plugin.codeInsight.JetFileReferencesResolver
import org.jetbrains.jet.lang.psi.JetSimpleNameExpression
import org.jetbrains.jet.lang.psi.JetThisExpression
import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.plugin.codeInsight.DescriptorToDeclarationUtil
import java.util.Collections
import org.jetbrains.jet.lang.psi.JetBlockExpression
import org.jetbrains.jet.renderer.DescriptorRenderer
import org.jetbrains.jet.lang.psi.psiUtil.getParentByTypeAndBranch
import org.jetbrains.jet.lang.psi.JetQualifiedExpression
import org.jetbrains.jet.lang.psi.psiUtil.isInsideOf
import java.util.ArrayList
import com.intellij.psi.PsiNamedElement
import org.jetbrains.jet.lang.psi.JetSuperExpression
import org.jetbrains.jet.lang.types.JetType
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
data class ResolveResult(
val originalRefExpr: JetSimpleNameExpression,
val declaration: PsiNamedElement,
val descriptor: DeclarationDescriptor,
val resolvedCall: ResolvedCall<*>?
)
data class ResolvedReferenceInfo(
val refExpr: JetSimpleNameExpression,
val offsetInBody: Int,
val resolveResult: ResolveResult
)
class ExtractionData(
val originalFile: JetFile,
val originalElements: List<PsiElement>,
val nextSibling: PsiElement
) {
val project: Project = originalFile.getProject()
fun getExpressions(): List<JetExpression> = originalElements.filterIsInstance(javaClass<JetExpression>())
fun getCodeFragmentTextRange(): TextRange? {
val originalElements = originalElements
return when (originalElements.size) {
0 -> null
1 -> originalElements.first!!.getTextRange()
else -> {
val from = originalElements.first!!.getTextRange()!!.getStartOffset()
val to = originalElements.last!!.getTextRange()!!.getEndOffset()
TextRange(from, to)
}
}
}
fun getCodeFragmentText(): String =
getCodeFragmentTextRange()?.let { originalFile.getText()!!.substring(it.getStartOffset(), it.getEndOffset()) } ?: ""
val originalStartOffset = originalElements.first?.let { e -> e.getTextRange()!!.getStartOffset() }
val refOffsetToDeclaration by Delegates.lazy {
if (originalStartOffset != null) {
val resultMap = HashMap<Int, ResolveResult>()
for ((ref, context) in JetFileReferencesResolver.resolve(originalFile, getExpressions())) {
if (ref !is JetSimpleNameExpression) continue
val resolvedCallKey = (ref.getParent() as? JetThisExpression) ?: ref
val resolvedCall = context[BindingContext.RESOLVED_CALL, resolvedCallKey]
val descriptor = context[BindingContext.REFERENCE_TARGET, ref]
if (descriptor == null) continue
val declaration = DescriptorToDeclarationUtil.getDeclaration(project, descriptor, context) as? PsiNamedElement
if (declaration == null) continue
val offset = ref.getTextRange()!!.getStartOffset() - originalStartOffset
resultMap[offset] = ResolveResult(ref, declaration, descriptor, resolvedCall)
}
resultMap
}
else Collections.emptyMap<Int, ResolveResult>()
}
fun getBrokenReferencesInfo(body: JetBlockExpression): List<ResolvedReferenceInfo> {
fun compareDescriptors(d1: DeclarationDescriptor?, d2: DeclarationDescriptor?): Boolean {
return d1 == d2 || (d1 != null && d2 != null && DescriptorRenderer.TEXT.render(d1) == DescriptorRenderer.TEXT.render(d2))
}
val startOffset = body.getStatements().first!!.getTextRange()!!.getStartOffset()
val referencesInfo = ArrayList<ResolvedReferenceInfo>()
for ((ref, context) in JetFileReferencesResolver.resolve(body)) {
if (ref !is JetSimpleNameExpression) continue
val parent = ref.getParent()
if (parent is JetQualifiedExpression
&& parent.getSelectorExpression() == ref
&& parent.getReceiverExpression() !is JetSuperExpression) continue
val offset = ref.getTextRange()!!.getStartOffset() - startOffset
refOffsetToDeclaration[offset]?.let { originalResolveResult ->
val descriptor = context[BindingContext.REFERENCE_TARGET, ref]
if (!compareDescriptors(originalResolveResult.descriptor, descriptor)
&& !originalResolveResult.declaration.isInsideOf(originalElements)) {
referencesInfo.add(ResolvedReferenceInfo(ref, offset, originalResolveResult))
}
}
}
return referencesInfo
}
fun getInferredResultType(): JetType? =
getExpressions().last?.let { AnalyzerFacadeWithCache.getContextForElement(it)[BindingContext.EXPRESSION_TYPE, it] }
}
@@ -0,0 +1,123 @@
/*
* 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.lang.types.JetType
import org.jetbrains.jet.lang.psi.JetPsiFactory
import org.jetbrains.jet.lang.psi.JetCallExpression
import org.jetbrains.jet.lang.psi.JetElement
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns
import com.intellij.util.containers.MultiMap
import com.intellij.psi.PsiElement
import org.jetbrains.jet.lang.psi.JetThisExpression
import org.jetbrains.jet.plugin.references.JetSimpleNameReference
import org.jetbrains.jet.lang.resolve.name.FqName
import org.jetbrains.jet.plugin.references.JetSimpleNameReference.ShorteningMode
data class Parameter(
val argumentText: String,
val name: String,
var mirrorVarName: String?,
val parameterType: JetType,
val receiverCandidate: Boolean
) {
val nameForRef: String get() = mirrorVarName ?: name
}
trait Replacement: Function1<JetElement, Unit>
trait ParameterReplacement : Replacement {
val parameter: Parameter
fun copy(parameter: Parameter): ParameterReplacement
}
class RenameReplacement(override val parameter: Parameter): ParameterReplacement {
override fun copy(parameter: Parameter) = RenameReplacement(parameter)
[suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")]
override fun invoke(e: JetElement) {
val thisExpr = e.getParent() as? JetThisExpression
(thisExpr ?: e).replace(JetPsiFactory.createSimpleName(e.getProject(), parameter.nameForRef))
}
}
class AddPrefixReplacement(override val parameter: Parameter): ParameterReplacement {
override fun copy(parameter: Parameter) = AddPrefixReplacement(parameter)
[suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")]
override fun invoke(e: JetElement) {
val selector = (e.getParent() as? JetCallExpression) ?: e
selector.replace(JetPsiFactory.createExpression(e.getProject(), "${parameter.nameForRef}.${selector.getText()}"))
}
}
class FqNameReplacement(val fqName: FqName): Replacement {
[suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")]
override fun invoke(e: JetElement) {
(e.getReference() as? JetSimpleNameReference)?.bindToFqName(fqName, ShorteningMode.NO_SHORTENING)
}
}
trait ControlFlow {
val returnType: JetType
}
object DefaultControlFlow: ControlFlow {
override val returnType: JetType get() = DEFAULT_RETURN_TYPE
}
trait JumpBasedControlFlow : ControlFlow {
val elementsToReplace: List<JetElement>
val elementToInsertAfterCall: JetElement
}
class ConditionalJump(
override val elementsToReplace: List<JetElement>,
override val elementToInsertAfterCall: JetElement
): JumpBasedControlFlow {
override val returnType: JetType get() = KotlinBuiltIns.getInstance().getBooleanType()
}
class UnconditionalJump(
override val elementsToReplace: List<JetElement>,
override val elementToInsertAfterCall: JetElement
): JumpBasedControlFlow {
override val returnType: JetType get() = KotlinBuiltIns.getInstance().getUnitType()
}
class ExpressionEvaluation(override val returnType: JetType): ControlFlow
class ExpressionEvaluationWithCallSiteReturn(override val returnType: JetType): ControlFlow
class ParameterUpdate(val parameter: Parameter): ControlFlow {
override val returnType: JetType get() = parameter.parameterType
}
data class ExtractionDescriptor(
val extractionData: ExtractionData,
val name: String,
val visibility: String,
val parameters: List<Parameter>,
val receiverParameter: Parameter?,
val replacementMap: Map<Int, Replacement>,
val controlFlow: ControlFlow
)
class ExtractionDescriptorWithConflicts(
val descriptor: ExtractionDescriptor,
val conflicts: MultiMap<PsiElement, String>
)
@@ -0,0 +1,626 @@
/*
* 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 com.intellij.psi.PsiElement
import org.jetbrains.jet.lang.types.JetType
import org.jetbrains.jet.lang.psi.JetNamedDeclaration
import org.jetbrains.jet.lang.psi.JetNamedFunction
import org.jetbrains.jet.renderer.DescriptorRenderer
import org.jetbrains.jet.lang.psi.JetSimpleNameExpression
import org.jetbrains.jet.lang.psi.JetPsiFactory
import java.util.HashMap
import org.jetbrains.jet.lang.psi.JetParameter
import com.intellij.util.containers.MultiMap
import org.jetbrains.jet.lang.psi.JetBlockExpression
import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor
import org.jetbrains.jet.lang.descriptors.CallableDescriptor
import org.jetbrains.jet.plugin.refactoring.JetRefactoringBundle
import org.jetbrains.jet.lang.cfg.pseudocode.PseudocodeUtil
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import org.jetbrains.jet.plugin.util.MaybeValue
import org.jetbrains.jet.plugin.util.Maybe
import org.jetbrains.jet.lang.psi.psiUtil.isInsideOf
import java.util.HashSet
import org.jetbrains.jet.lang.cfg.pseudocode.WriteValueInstruction
import org.jetbrains.jet.lang.descriptors.VariableDescriptor
import org.jetbrains.jet.plugin.util.MaybeError
import org.jetbrains.jet.lang.psi.psiUtil.appendElement
import org.jetbrains.jet.plugin.refactoring.createTempCopy
import org.jetbrains.jet.lang.psi.psiUtil.getParentByType
import org.jetbrains.jet.lang.psi.JetElement
import org.jetbrains.jet.plugin.refactoring.JetNameSuggester
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns
import org.jetbrains.jet.lang.psi.JetProperty
import org.jetbrains.jet.lang.psi.psiUtil.prependElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.jet.lang.cfg.pseudocode.JetElementInstruction
import org.jetbrains.jet.lang.cfg.pseudocode.AbstractJumpInstruction
import org.jetbrains.jet.lang.resolve.scopes.receivers.ReceiverValue
import org.jetbrains.jet.lang.resolve.scopes.receivers.ThisReceiver
import org.jetbrains.jet.lang.descriptors.ClassDescriptor
import org.jetbrains.jet.lang.psi.JetThisExpression
import org.jetbrains.jet.lang.psi.JetDeclarationWithBody
import org.jetbrains.jet.lang.cfg.pseudocode.Instruction
import java.util.ArrayList
import org.jetbrains.jet.lang.cfg.pseudocode.ReadValueInstruction
import org.jetbrains.jet.lang.cfg.pseudocode.CallInstruction
import org.jetbrains.jet.lang.types.CommonSupertypes
import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.lang.cfg.pseudocode.ReturnValueInstruction
import org.jetbrains.jet.lang.cfg.Label
import org.jetbrains.jet.lang.psi.JetReturnExpression
import org.jetbrains.jet.lang.psi.JetBreakExpression
import org.jetbrains.jet.lang.psi.JetContinueExpression
import org.jetbrains.jet.lang.psi.JetPsiFactory.FunctionBuilder
import org.jetbrains.jet.plugin.refactoring.JetNameValidatorImpl
import org.jetbrains.jet.lang.psi.JetExpression
import org.jetbrains.jet.plugin.codeInsight.DescriptorToDeclarationUtil
import org.jetbrains.jet.lang.psi.JetThrowExpression
import org.jetbrains.jet.plugin.imports.canBeReferencedViaImport
import org.jetbrains.jet.plugin.codeInsight.ShortenReferences
import org.jetbrains.jet.lang.cfg.pseudocode.LocalFunctionDeclarationInstruction
import org.jetbrains.jet.lang.descriptors.ClassKind
import org.jetbrains.jet.lang.psi.JetSuperExpression
import org.jetbrains.jet.lang.psi.JetQualifiedExpression
import org.jetbrains.jet.lang.psi.JetCallExpression
import org.jetbrains.jet.lang.psi.JetTreeVisitorVoid
import org.jetbrains.jet.lang.types.checker.JetTypeChecker
import org.jetbrains.jet.lang.resolve.name.FqName
import org.jetbrains.jet.lang.resolve.DescriptorUtils
private val DEFAULT_FUNCTION_NAME = "myFun"
private val DEFAULT_RETURN_TYPE = KotlinBuiltIns.getInstance().getUnitType()
private val DEFAULT_PARAMETER_TYPE = KotlinBuiltIns.getInstance().getAnyType()
private fun JetType.isDefault(): Boolean = KotlinBuiltIns.getInstance().isUnit(this)
private fun List<Instruction>.getModifiedVarDescriptors(bindingContext: BindingContext): Set<VariableDescriptor> {
return this
.map {if (it is WriteValueInstruction) PseudocodeUtil.extractVariableDescriptorIfAny(it, true, bindingContext) else null}
.filterNotNullTo(HashSet<VariableDescriptor>())
}
private fun List<Instruction>.getExitPoints(): List<Instruction> =
filter { localInstruction -> localInstruction.getNextInstructions().any { it !in this } }
private fun List<Instruction>.getResultType(bindingContext: BindingContext): JetType {
fun instructionToType(instruction: Instruction): JetType? {
return when (instruction) {
is ReadValueInstruction -> {
PseudocodeUtil.extractVariableDescriptorIfAny(instruction, true, bindingContext)?.getType()
}
is CallInstruction -> {
instruction.resolvedCall.getResultingDescriptor()?.getReturnType()
}
is ReturnValueInstruction -> {
val expression = (instruction.getElement() as JetReturnExpression).getReturnedExpression()
bindingContext[BindingContext.EXPRESSION_TYPE, expression]
}
else -> null
}
}
val resultTypes = map(::instructionToType).filterNotNull()
return if (resultTypes.isNotEmpty()) CommonSupertypes.commonSupertype(resultTypes) else DEFAULT_RETURN_TYPE
}
private fun List<AbstractJumpInstruction>.checkEquivalence(checkPsi: Boolean): Boolean {
if (mapTo(HashSet<Label?>()) { it.getTargetLabel() }.size > 1) return false
return !checkPsi || mapTo(HashSet<String?>()) { it.getElement().getText() }.size <= 1
}
private fun List<Instruction>.analyzeControlFlow(
bindingContext: BindingContext,
parameters: Set<Parameter>,
inferredType: JetType?
): Maybe<ControlFlow, String> {
val outParameters = parameters.filterTo(HashSet<Parameter>()) { it.mirrorVarName != null }
if (outParameters.size > 1) {
val outValuesStr = outParameters.map { it.argumentText }.sort().makeString(separator = "\n")
return MaybeError("${JetRefactoringBundle.message("selected.code.fragment.has.multiple.output.values")!!}\n$outValuesStr")
}
val exitPoints = getExitPoints()
val valuedReturnExits = ArrayList<ReturnValueInstruction>()
val defaultExits = ArrayList<Instruction>()
val jumpExits = ArrayList<AbstractJumpInstruction>()
exitPoints.forEach {
when (it) {
is ReturnValueInstruction -> valuedReturnExits.add(it)
is AbstractJumpInstruction -> {
val element = it.getElement()
if (element is JetReturnExpression
|| element is JetBreakExpression
|| element is JetContinueExpression
|| element is JetThrowExpression) {
jumpExits.add(it)
}
else {
defaultExits.add(it)
}
}
else -> if (it !is LocalFunctionDeclarationInstruction) {
defaultExits.add(it)
}
}
}
val builtins = KotlinBuiltIns.getInstance()
val hasMeaningfulType = inferredType != null && !builtins.isUnit(inferredType) && !builtins.isNothing(inferredType)
if (outParameters.isNotEmpty()) {
if (hasMeaningfulType || valuedReturnExits.isNotEmpty() || jumpExits.isNotEmpty()) {
return MaybeError(JetRefactoringBundle.message("selected.code.fragment.has.output.values.and.exit.points")!!)
}
return MaybeValue(ParameterUpdate(outParameters.first()))
}
val multipleExitsError =
MaybeError<ControlFlow, String>(JetRefactoringBundle.message("selected.code.fragment.has.multiple.exit.points")!!)
if (hasMeaningfulType) {
if (valuedReturnExits.isNotEmpty() || jumpExits.isNotEmpty()) return multipleExitsError
return MaybeValue(ExpressionEvaluation(inferredType!!))
}
if (valuedReturnExits.isNotEmpty()) {
if (jumpExits.isNotEmpty()) return multipleExitsError
if (defaultExits.isNotEmpty()) {
if (valuedReturnExits.size != 1) return multipleExitsError
val element = valuedReturnExits.first!!.getElement()
return MaybeValue(ConditionalJump(listOf(element), element))
}
if (!valuedReturnExits.checkEquivalence(false)) return multipleExitsError
return MaybeValue(ExpressionEvaluationWithCallSiteReturn(valuedReturnExits.getResultType(bindingContext)))
}
if (jumpExits.isNotEmpty()) {
if (!jumpExits.checkEquivalence(true)) return multipleExitsError
val elements = jumpExits.map { it.getElement() }
if (defaultExits.isNotEmpty()) return MaybeValue(ConditionalJump(elements, elements.first!!))
return MaybeValue(UnconditionalJump(elements, elements.first!!))
}
return MaybeValue(DefaultControlFlow)
}
private fun ExtractionData.createTemporaryCodeBlock(): JetBlockExpression {
val position = nextSibling.getTextRange()!!.getStartOffset()
val tmpFile = originalFile.createTempCopy { text ->
StringBuilder(text).insert(position, "fun() {\n${getCodeFragmentText()}\n}\n").toString()
}
val tmpFunction = tmpFile.findElementAt(position)?.getParentByType(javaClass<JetNamedFunction>())!!
return tmpFunction.getBodyExpression() as JetBlockExpression
}
private fun ExtractionData.inferParametersInfo(
commonParent: PsiElement,
localInstructions: List<Instruction>,
bindingContext: BindingContext,
resultType: JetType?,
replacementMap: MutableMap<Int, Replacement>,
parameters: MutableSet<Parameter>
): MaybeError<ExtractionDescriptor, String>? {
val varNameValidator = JetNameValidatorImpl(commonParent.getParentByType(javaClass<JetExpression>()), originalElements.first, true)
val modifiedVarDescriptors = localInstructions.getModifiedVarDescriptors(bindingContext)
val extractedDescriptorToParameter = HashMap<DeclarationDescriptor, Parameter>()
val nonDenotableTypes = HashSet<JetType>()
for (refInfo in getBrokenReferencesInfo(createTemporaryCodeBlock())) {
val (originalRef, originalDeclaration, originalDescriptor, resolvedCall) = refInfo.resolveResult
val ref = refInfo.refExpr
val selector = (ref.getParent() as? JetCallExpression) ?: ref
val superExpr = (selector.getParent() as? JetQualifiedExpression)?.getReceiverExpression() as? JetSuperExpression
if (superExpr != null) {
return MaybeError(JetRefactoringBundle.message("cannot.extract.super.call")!!)
}
val receiverArgument = resolvedCall?.getReceiverArgument()
val receiver = when(receiverArgument) {
ReceiverValue.NO_RECEIVER -> resolvedCall?.getThisObject()
else -> receiverArgument
} ?: ReceiverValue.NO_RECEIVER
val thisDescriptor = (receiver as? ThisReceiver)?.getDeclarationDescriptor()
val hasThisReceiver = thisDescriptor != null
val thisExpr = ref.getParent() as? JetThisExpression
val hasClassObjectReceiver = (thisDescriptor as? ClassDescriptor)?.getKind() == ClassKind.CLASS_OBJECT
val classObjectClassDescriptor =
if (hasClassObjectReceiver) thisDescriptor!!.getContainingDeclaration() as? ClassDescriptor else null
if (classObjectClassDescriptor != null) {
if (!classObjectClassDescriptor.canBeReferencedViaImport()) {
return MaybeError(JetRefactoringBundle.message("cannot.extract.non.local.declaration.ref")!!)
}
replacementMap[refInfo.offsetInBody] = FqNameReplacement(DescriptorUtils.getFqNameSafe(originalDescriptor))
}
else {
val extractThis = hasThisReceiver || thisExpr != null
val extractLocalVar =
(originalDeclaration is JetProperty && originalDeclaration.isLocal()) || originalDeclaration is JetParameter
val descriptorToExtract = (if (extractThis) thisDescriptor else null) ?: originalDescriptor
val extractParameter = extractThis || extractLocalVar
if (extractParameter) {
val parameterType =
if (hasThisReceiver) {
when (descriptorToExtract) {
is ClassDescriptor -> descriptorToExtract.getDefaultType()
is CallableDescriptor -> descriptorToExtract.getReceiverParameter()?.getType()
else -> null
} ?: DEFAULT_PARAMETER_TYPE
}
else bindingContext[BindingContext.EXPRESSION_TYPE, originalRef] ?: DEFAULT_PARAMETER_TYPE
if (!parameterType.canBeReferencedViaImport()) {
nonDenotableTypes.add(parameterType)
continue
}
val existingParameter = extractedDescriptorToParameter[descriptorToExtract]
val parameter: Parameter =
if (existingParameter != null) {
if (!JetTypeChecker.INSTANCE.equalTypes(existingParameter.parameterType, parameterType)) {
val newParameter = existingParameter.copy(
parameterType = CommonSupertypes.commonSupertype(listOf(existingParameter.parameterType, parameterType))
)
extractedDescriptorToParameter[descriptorToExtract] = newParameter
for ((offset, replacement) in replacementMap) {
if (replacement is ParameterReplacement && replacement.parameter == existingParameter) {
replacementMap[offset] = replacement.copy(newParameter)
}
}
newParameter
}
else existingParameter
}
else {
val parameterName: String =
if (extractThis) {
JetNameSuggester.suggestNames(parameterType, varNameValidator, null).first()
}
else originalDeclaration.getName()!!
val mirrorVarName: String?
if (descriptorToExtract in modifiedVarDescriptors) {
mirrorVarName = varNameValidator.validateName(parameterName)
}
else {
mirrorVarName = null
}
val argumentText =
if (hasThisReceiver && extractThis)
"this@${parameterType.getConstructor().getDeclarationDescriptor()!!.getName().asString()}"
else
(thisExpr ?: ref).getText()!!
val parameter = Parameter(argumentText, parameterName, mirrorVarName, parameterType, extractThis)
extractedDescriptorToParameter[descriptorToExtract] = parameter
parameter
}
replacementMap[refInfo.offsetInBody] =
if (hasThisReceiver && extractThis) AddPrefixReplacement(parameter) else RenameReplacement(parameter)
}
else if (originalDeclaration is JetProperty) {
return MaybeError(JetRefactoringBundle.message("cannot.extract.non.local.declaration.ref")!!)
}
}
}
if (resultType != null && !resultType.canBeReferencedViaImport()) {
nonDenotableTypes.add(resultType)
}
if (nonDenotableTypes.isNotEmpty()) {
val typeStr = nonDenotableTypes.map {
DescriptorRenderer.HTML.renderType(it)
}.sort().makeString(separator = "\n")
return MaybeError("${JetRefactoringBundle.message("parameter.types.are.not.denotable")!!}\n$typeStr")
}
parameters.addAll(extractedDescriptorToParameter.values())
return null
}
private fun ExtractionData.checkLocalDeclarationsWithNonLocalUsages(
allInstructions: List<Instruction>,
localInstructions: List<Instruction>,
bindingContext: BindingContext
): MaybeError<ExtractionDescriptor, String>? {
// todo: non-locally used declaration can be turned into the output value
val declarations = ArrayList<JetNamedDeclaration>()
for (instruction in allInstructions) {
if (instruction in localInstructions) continue
PseudocodeUtil.extractVariableDescriptorIfAny(instruction, true, bindingContext)?.let { descriptor ->
val declaration = DescriptorToDeclarationUtil.getDeclaration(project, descriptor, bindingContext)
if (declaration is JetNamedDeclaration && declaration.isInsideOf(originalElements)) {
declarations.add(declaration)
}
}
}
if (declarations.isNotEmpty()) {
val localVarStr = declarations.map { it.getName()!! }.sort().makeString(separator = "\n")
return MaybeError("${JetRefactoringBundle.message("variables.are.used.outside.of.selected.code.fragment")!!}\n$localVarStr")
}
return null
}
private fun ExtractionData.checkDeclarationsMovingOutOfScope(controlFlow: ControlFlow): MaybeError<ExtractionDescriptor, String>? {
val declarationsOutOfScope = HashSet<JetNamedDeclaration>()
if (controlFlow is JumpBasedControlFlow) {
controlFlow.elementToInsertAfterCall.accept(
object: JetTreeVisitorVoid() {
override fun visitSimpleNameExpression(expression: JetSimpleNameExpression) {
val target = expression.getReference()?.resolve()
if (target is JetNamedDeclaration && target.isInsideOf(originalElements)) {
declarationsOutOfScope.add(target)
}
}
}
)
}
if (declarationsOutOfScope.isNotEmpty()) {
val declStr = declarationsOutOfScope.map { it.getName()!! }.sort().makeString(separator = "\n")
return MaybeError("${JetRefactoringBundle.message("declarations.will.move.out.of.scope")!!}\n$declStr")
}
return null
}
fun ExtractionData.performAnalysis(): Maybe<ExtractionDescriptor, String> {
if (originalElements.empty) return MaybeError(JetRefactoringBundle.message("cannot.refactor.no.expresson")!!)
val commonParent = PsiTreeUtil.findCommonParent(originalElements)!!
val enclosingDeclaration = commonParent.getParentByType(javaClass<JetDeclarationWithBody>())
if (enclosingDeclaration == null) return MaybeError(JetRefactoringBundle.message("cannot.refactor.no.container")!!)
val resolveSession = AnalyzerFacadeWithCache.getLazyResolveSessionForFile(originalFile)
val bindingContext = resolveSession.resolveToElement(enclosingDeclaration.getBodyExpression())
val pseudocode = PseudocodeUtil.generatePseudocode(enclosingDeclaration, bindingContext)
val localInstructions = pseudocode.getInstructions().filter {
it is JetElementInstruction && it.getElement().isInsideOf(originalElements)
}
val inferredResultType = getInferredResultType()
val replacementMap = HashMap<Int, Replacement>()
val parameters = HashSet<Parameter>()
val parameterError =
inferParametersInfo(commonParent, localInstructions, bindingContext, inferredResultType, replacementMap, parameters)
if (parameterError != null) return parameterError
val controlFlowInfo = localInstructions.analyzeControlFlow(bindingContext, parameters, inferredResultType)
if (controlFlowInfo is MaybeError) return MaybeError(controlFlowInfo.error)
val controlFlow = (controlFlowInfo as MaybeValue).value
val localDeclError = checkLocalDeclarationsWithNonLocalUsages(pseudocode.getInstructions(), localInstructions, bindingContext)
if (localDeclError != null) return localDeclError
val outOfScopeError = checkDeclarationsMovingOutOfScope(controlFlow)
if (outOfScopeError != null) return outOfScopeError
val functionNameValidator = JetNameValidatorImpl(nextSibling.getParent(), nextSibling, false)
val functionName = JetNameSuggester.suggestNames(controlFlow.returnType, functionNameValidator, DEFAULT_FUNCTION_NAME).first()
val receiverCandidates = parameters.filterTo(HashSet<Parameter>()) { it.receiverCandidate }
val receiverParameter = if (receiverCandidates.size == 1) receiverCandidates.first() else null
receiverParameter?.let { parameters.remove(it) }
return MaybeValue(
ExtractionDescriptor(this, functionName, "", parameters.sortBy { it.name }, receiverParameter, replacementMap, controlFlow)
)
}
fun ExtractionDescriptor.validate(): ExtractionDescriptorWithConflicts {
// todo: add name conflict checks (parameters, function, etc.)
return ExtractionDescriptorWithConflicts(this, MultiMap<PsiElement, String>())
}
fun ExtractionDescriptor.getFunctionText(
withBody: Boolean = true,
descriptorRenderer: DescriptorRenderer = DescriptorRenderer.TEXT
): String {
return FunctionBuilder().let { builder ->
builder.modifier(visibility)
receiverParameter?.let { builder.receiver(descriptorRenderer.renderType(it.parameterType)) } ?: builder.noReceiver()
builder.name(name)
parameters.forEach { parameter ->
builder.param(parameter.name, descriptorRenderer.renderType(parameter.parameterType))
}
with(controlFlow.returnType) {
if (isDefault()) builder.noReturnType() else builder.returnType(descriptorRenderer.renderType(this))
}
if (withBody) {
builder.blockBody(extractionData.getCodeFragmentText())
}
builder.toFunctionText()
}
}
fun ExtractionDescriptor.generateFunction(inTempFile: Boolean = false): JetNamedFunction {
val project = extractionData.project
fun createFunction(): JetNamedFunction {
return with(extractionData) {
if (inTempFile) {
val position = nextSibling.getTextRange()!!.getStartOffset()
val tmpFile = originalFile.createTempCopy { text ->
StringBuilder(text).insert(position, getFunctionText() + "\n").toString()
}
tmpFile.findElementAt(position)?.getParentByType(javaClass<JetNamedFunction>())!!
}
else {
JetPsiFactory.createFunction(project, getFunctionText())
}
}
}
fun adjustFunctionBody(function: JetNamedFunction) {
val body = function.getBodyExpression() as JetBlockExpression
val exprReplacementMap = HashMap<JetElement, (JetElement) -> Unit>()
val range = body.getStatements().first?.getTextRange()
if (range == null) return
val bodyOffset = range.getStartOffset()
val file = body.getContainingFile()!!
for ((offsetInBody, replacement) in replacementMap) {
if (replacement is ParameterReplacement && replacement.parameter == receiverParameter) continue
val expr = file.findElementAt(bodyOffset + offsetInBody)?.getParentByType(javaClass<JetSimpleNameExpression>())
assert(expr != null, "Couldn't find expression at $offsetInBody in '${body.getText()}'")
exprReplacementMap[expr!!] = replacement
}
if (controlFlow is JumpBasedControlFlow) {
val replacementExpr = JetPsiFactory.createExpression(
project,
if (controlFlow is ConditionalJump) "return true" else "return"
)
for (jumpElement in controlFlow.elementsToReplace) {
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()}'")
exprReplacementMap[expr!!] = { it.replace(replacementExpr) }
}
}
for ((expr, replacement) in exprReplacementMap) {
replacement(expr)
}
for (param in parameters) {
param.mirrorVarName?.let { varName ->
body.prependElement(JetPsiFactory.createProperty(project, varName, null, true, param.name))
}
}
when (controlFlow) {
is ParameterUpdate ->
body.appendElement(JetPsiFactory.createReturn(project, controlFlow.parameter.nameForRef))
is ConditionalJump ->
body.appendElement(JetPsiFactory.createReturn(project, "false"))
is ExpressionEvaluation ->
body.getStatements().last?.let { it.replace(JetPsiFactory.createReturn(project, it.getText()!!)) }
}
}
fun insertFunction(function: JetNamedFunction): JetNamedFunction {
return with(extractionData) {
val targetContainer = nextSibling.getParent()!!
val functionInFile = targetContainer.addBefore(function, nextSibling) as JetNamedFunction
targetContainer.addBefore(JetPsiFactory.createWhiteSpace(project, "\n\n"), nextSibling)
functionInFile
}
}
fun makeCall(function: JetNamedFunction): JetNamedFunction {
val anchor = extractionData.originalElements.first
if (anchor == null) return function
anchor.getNextSibling()?.let { from ->
val to = extractionData.originalElements.last
if (to != anchor) {
anchor.getParent()!!.deleteChildRange(from, to);
}
}
val callText = parameters
.map { it.argumentText }
.makeString(separator = ", ", prefix = "$name(", postfix = ")")
val wrappedCall = when (controlFlow) {
is ExpressionEvaluationWithCallSiteReturn ->
JetPsiFactory.createReturn(project, callText)
is ParameterUpdate ->
JetPsiFactory.createExpression(project, "${controlFlow.parameter.argumentText} = $callText")
is ConditionalJump ->
JetPsiFactory.createExpression(project, "if ($callText) ${controlFlow.elementToInsertAfterCall.getText()}")
is UnconditionalJump -> {
val anchorParent = anchor.getParent()!!
anchorParent.addAfter(
JetPsiFactory.createExpression(project, controlFlow.elementToInsertAfterCall.getText()),
anchor
)
anchorParent.addAfter(JetPsiFactory.createNewLine(project), anchor)
JetPsiFactory.createExpression(project, callText)
}
else ->
JetPsiFactory.createExpression(project, callText)
}
anchor.replace(wrappedCall)
return function
}
val function = createFunction()
adjustFunctionBody(function)
if (inTempFile) return function
val functionInPlace = makeCall(insertFunction(function))
ShortenReferences.process(functionInPlace)
return functionInPlace
}
@@ -16,25 +16,17 @@
package org.jetbrains.jet.plugin.refactoring
import org.jetbrains.jet.lang.psi.JetSimpleNameExpression
import org.jetbrains.jet.lang.resolve.name.FqName
import org.jetbrains.jet.lang.psi.JetElement
import org.jetbrains.jet.lang.psi.JetCallExpression
import org.jetbrains.jet.lang.psi.JetPsiFactory
import org.jetbrains.jet.lang.psi.psiUtil.getQualifiedElement
import org.jetbrains.jet.lang.psi.JetUserType
import org.jetbrains.jet.lang.resolve.name.isOneSegmentFQN
import com.intellij.psi.PsiElement
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiDirectory
import org.jetbrains.jet.lang.psi.JetFile
import com.intellij.openapi.roots.JavaProjectRootsUtil
import com.intellij.psi.PsiPackage
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMember
import org.jetbrains.jet.lang.psi.JetPsiUtil
import org.jetbrains.jet.asJava.namedUnwrappedElement
import org.jetbrains.jet.lang.psi.JetNamedDeclaration
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.refactoring.util.ConflictsUtil
import org.jetbrains.jet.lang.psi.psiUtil.getPackage
@@ -46,6 +38,17 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiFile
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.jet.lang.psi.*
import java.util.ArrayList
import com.intellij.openapi.application.ApplicationManager
import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException
import com.intellij.refactoring.ui.ConflictsDialog
import com.intellij.util.containers.MultiMap
import com.intellij.openapi.command.CommandProcessor
import org.jetbrains.jet.lexer.JetTokens
import org.jetbrains.jet.lang.psi.psiUtil.getParentByType
import org.jetbrains.jet.plugin.codeInsight.TipsManager
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
/**
* Replace [[JetSimpleNameExpression]] (and its enclosing qualifier) with qualified element given by FqName
@@ -120,4 +123,11 @@ public fun PsiElement.getUsageContext(): PsiElement {
}
public fun PsiElement.isInJavaSourceRoot(): Boolean =
!JavaProjectRootsUtil.isOutsideJavaSourceRoot(getContainingFile())
!JavaProjectRootsUtil.isOutsideJavaSourceRoot(getContainingFile())
public inline fun JetFile.createTempCopy(textTransform: (String) -> String): JetFile {
val tmpFile = JetPsiFactory.createFile(getProject(), getName(), textTransform(getText() ?: ""))
tmpFile.setOriginalFile(this)
return tmpFile
}