Extract Function: Allow the user to proceed with extraction when non-critical errors are found

#KT-4941 Fixed
This commit is contained in:
Alexey Sedunov
2014-04-29 15:07:59 +04:00
parent 1e49dcda27
commit 5cfb8bbf99
4 changed files with 120 additions and 60 deletions
@@ -62,6 +62,7 @@ import org.jetbrains.jet.plugin.caches.resolve.getAnalysisResults
import org.jetbrains.jet.lang.psi.JetCodeFragment
import org.jetbrains.jet.lang.psi.JetImportList
import org.jetbrains.jet.lang.psi.JetExpression
import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status
object KotlinEvaluationBuilder: EvaluatorBuilder {
override fun build(codeFragment: PsiElement, position: SourcePosition?): ExpressionEvaluator {
@@ -286,11 +287,11 @@ private fun getFunctionForExtractedFragment(
if (nextSibling == null) return null
val analysisResult = ExtractionData(tmpFile, Collections.singletonList(newDebugExpression), nextSibling).performAnalysis()
if (analysisResult is MaybeError) {
throw EvaluateExceptionUtil.createEvaluateException(analysisResult.error)
if (analysisResult.status != Status.SUCCESS) {
throw EvaluateExceptionUtil.createEvaluateException(analysisResult.messages.makeString("\n"))
}
val validationResult = (analysisResult as MaybeValue).value.validate()
val validationResult = analysisResult.descriptor!!.validate()
if (!validationResult.conflicts.isEmpty()) {
throw EvaluateExceptionUtil.createEvaluateException("Some declarations are unavailable")
}
@@ -35,19 +35,21 @@ import org.jetbrains.jet.plugin.refactoring.getAllExtractionContainers
import com.intellij.refactoring.IntroduceTargetChooser
import com.intellij.openapi.util.Pass
import com.intellij.openapi.application.ApplicationManager
import org.jetbrains.jet.lang.psi.JetPsiFactory
import org.jetbrains.jet.lang.psi.psiUtil.getOutermostParentContainedIn
import org.jetbrains.jet.plugin.refactoring.checkConflictsInteractively
import org.jetbrains.jet.plugin.refactoring.executeWriteCommand
import org.jetbrains.jet.plugin.util.MaybeError
import org.jetbrains.jet.plugin.util.MaybeValue
import org.jetbrains.jet.lang.psi.JetBlockExpression
import org.jetbrains.jet.lang.psi.JetClassOrObject
import org.jetbrains.jet.lang.psi.JetClassBody
import org.jetbrains.jet.lang.psi.JetNamedFunction
import org.jetbrains.jet.plugin.codeInsight.ShortenReferences
import kotlin.test.fail
import org.jetbrains.jet.lang.psi.JetDeclarationWithBody
import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.MessageType
import javax.swing.event.HyperlinkEvent
import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException
import com.intellij.ui.awt.RelativePoint
import com.intellij.openapi.ui.popup.Balloon.Position
public class ExtractKotlinFunctionHandler : RefactoringActionHandler {
fun doInvoke(
@@ -59,26 +61,59 @@ public class ExtractKotlinFunctionHandler : RefactoringActionHandler {
val project = file.getProject()
val analysisResult = ExtractionData(file, elements, nextSibling).performAnalysis()
if (analysisResult is MaybeError) {
showErrorHint(project, editor, analysisResult.error)
return
if (ApplicationManager.getApplication()!!.isUnitTestMode() && analysisResult.status != Status.SUCCESS) {
throw ConflictsInTestsException(analysisResult.messages)
}
val validationResult = (analysisResult as MaybeValue).value.validate()
if (!project.checkConflictsInteractively(validationResult.conflicts)) return
fun proceedWithExtraction() {
val validationResult = analysisResult.descriptor!!.validate()
if (!project.checkConflictsInteractively(validationResult.conflicts)) return
val descriptor =
if (ApplicationManager.getApplication()!!.isUnitTestMode()) {
validationResult.descriptor
}
else {
val dialog = KotlinExtractFunctionDialog(project, validationResult)
if (!dialog.showAndGet()) return
val descriptor =
if (ApplicationManager.getApplication()!!.isUnitTestMode()) {
validationResult.descriptor
}
else {
val dialog = KotlinExtractFunctionDialog(project, validationResult)
if (!dialog.showAndGet()) return
dialog.getCurrentDescriptor()
}
dialog.getCurrentDescriptor()
}
project.executeWriteCommand(EXTRACT_FUNCTION) { descriptor.generateFunction() }
project.executeWriteCommand(EXTRACT_FUNCTION) { descriptor.generateFunction() }
}
val message = analysisResult.messages.makeString("\n")
when (analysisResult.status) {
Status.CRITICAL_ERROR -> {
showErrorHint(project, editor, message)
}
Status.NON_CRITICAL_ERROR -> {
val anchorPoint = RelativePoint(
editor.getContentComponent(),
editor.visualPositionToXY(editor.getSelectionModel().getSelectionStartPosition()!!)
)
JBPopupFactory.getInstance()!!
.createHtmlTextBalloonBuilder(
"$message<br/><br/><a href=\"EXTRACT\">Proceed with extraction</a>",
MessageType.WARNING,
{ event ->
if (event?.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
proceedWithExtraction()
}
}
)
.setHideOnClickOutside(true)
.setHideOnFrameResize(false)
.setHideOnLinkClick(true)
.createBalloon()
.show(anchorPoint, Position.below)
}
Status.SUCCESS -> proceedWithExtraction()
}
}
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) {
@@ -31,6 +31,8 @@ import org.jetbrains.jet.lang.psi.psiUtil.replaced
import org.jetbrains.jet.lang.psi.JetQualifiedExpression
import org.jetbrains.jet.lang.psi.JetTypeParameter
import org.jetbrains.jet.lang.psi.JetTypeConstraint
import kotlin.properties.Delegates
import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status
data class Parameter(
val argumentText: String,
@@ -132,6 +134,18 @@ data class ExtractionDescriptor(
val controlFlow: ControlFlow
)
class AnalysisResult (
val descriptor: ExtractionDescriptor?,
val status: Status,
val messages: List<String>
) {
enum class Status {
SUCCESS
NON_CRITICAL_ERROR
CRITICAL_ERROR
}
}
class ExtractionDescriptorWithConflicts(
val descriptor: ExtractionDescriptor,
val conflicts: MultiMap<PsiElement, String>
@@ -31,13 +31,10 @@ 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.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
@@ -92,7 +89,6 @@ import org.jetbrains.jet.lang.descriptors.TypeParameterDescriptor
import org.jetbrains.jet.utils.DFS
import org.jetbrains.jet.utils.DFS.Neighbors
import org.jetbrains.jet.utils.DFS.VisitedWithSet
import org.jetbrains.jet.utils.DFS.AbstractNodeHandler
import org.jetbrains.jet.utils.DFS.CollectingNodeHandler
import org.jetbrains.jet.lang.resolve.BindingContextUtils
import org.jetbrains.jet.plugin.caches.resolve.getLazyResolveSession
@@ -100,6 +96,7 @@ import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import org.jetbrains.jet.lang.diagnostics.Errors
import org.jetbrains.jet.lang.psi.JetTypeReference
import org.jetbrains.jet.lang.psi.JetTypeParameterListOwner
import org.jetbrains.jet.plugin.refactoring.extractFunction.AnalysisResult.Status
import org.jetbrains.jet.lang.descriptors.FunctionDescriptor
private val DEFAULT_FUNCTION_NAME = "myFun"
@@ -147,13 +144,16 @@ private fun List<Instruction>.analyzeControlFlow(
bindingContext: BindingContext,
parameters: Set<Parameter>,
inferredType: JetType?
): Maybe<ControlFlow, String> {
): Pair<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")
return Pair(
DefaultControlFlow,
"${JetRefactoringBundle.message("selected.code.fragment.has.multiple.output.values")!!}\n$outValuesStr"
)
}
val exitPoints = getExitPoints()
@@ -188,19 +188,21 @@ private fun List<Instruction>.analyzeControlFlow(
if (outParameters.isNotEmpty()) {
if (hasMeaningfulType || valuedReturnExits.isNotEmpty() || jumpExits.isNotEmpty()) {
return MaybeError(JetRefactoringBundle.message("selected.code.fragment.has.output.values.and.exit.points")!!)
return Pair(DefaultControlFlow, JetRefactoringBundle.message("selected.code.fragment.has.output.values.and.exit.points")!!)
}
return MaybeValue(ParameterUpdate(outParameters.first()))
return Pair(ParameterUpdate(outParameters.first()), null)
}
val multipleExitsError =
MaybeError<ControlFlow, String>(JetRefactoringBundle.message("selected.code.fragment.has.multiple.exit.points")!!)
val multipleExitsError = Pair(
DefaultControlFlow,
JetRefactoringBundle.message("selected.code.fragment.has.multiple.exit.points")!!
)
if (hasMeaningfulType) {
if (valuedReturnExits.isNotEmpty() || jumpExits.isNotEmpty()) return multipleExitsError
return MaybeValue(ExpressionEvaluation(inferredType!!))
return Pair(ExpressionEvaluation(inferredType!!), null)
}
if (valuedReturnExits.isNotEmpty()) {
@@ -210,22 +212,22 @@ private fun List<Instruction>.analyzeControlFlow(
if (valuedReturnExits.size != 1) return multipleExitsError
val element = valuedReturnExits.first!!.getElement()
return MaybeValue(ConditionalJump(listOf(element), element))
return Pair(ConditionalJump(listOf(element), element), null)
}
if (!valuedReturnExits.checkEquivalence(false)) return multipleExitsError
return MaybeValue(ExpressionEvaluationWithCallSiteReturn(valuedReturnExits.getResultType(bindingContext)))
return Pair(ExpressionEvaluationWithCallSiteReturn(valuedReturnExits.getResultType(bindingContext)), null)
}
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!!))
if (defaultExits.isNotEmpty()) return Pair(ConditionalJump(elements, elements.first!!), null)
return Pair(UnconditionalJump(elements, elements.first!!), null)
}
return MaybeValue(DefaultControlFlow)
return Pair(DefaultControlFlow, null)
}
private fun ExtractionData.createTemporaryCodeBlock(): JetBlockExpression {
@@ -306,7 +308,7 @@ private fun ExtractionData.inferParametersInfo(
replacementMap: MutableMap<Int, Replacement>,
parameters: MutableSet<Parameter>,
typeParameters: MutableSet<TypeParameter>
): MaybeError<ExtractionDescriptor, String>? {
): String? {
val varNameValidator = JetNameValidatorImpl(
commonParent.getParentByType(javaClass<JetExpression>()),
originalElements.first,
@@ -324,7 +326,7 @@ private fun ExtractionData.inferParametersInfo(
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")!!)
return JetRefactoringBundle.message("cannot.extract.super.call")!!
}
val receiverArgument = resolvedCall?.getReceiverArgument()
@@ -343,7 +345,7 @@ private fun ExtractionData.inferParametersInfo(
if (classObjectClassDescriptor != null) {
if (!classObjectClassDescriptor.canBeReferencedViaImport()) {
return MaybeError(JetRefactoringBundle.message("cannot.extract.non.local.declaration.ref")!!)
return JetRefactoringBundle.message("cannot.extract.non.local.declaration.ref")!!
}
replacementMap[refInfo.offsetInBody] = FqNameReplacement(DescriptorUtils.getFqNameSafe(originalDescriptor))
@@ -367,6 +369,8 @@ private fun ExtractionData.inferParametersInfo(
}
else bindingContext[BindingContext.EXPRESSION_TYPE, originalRef] ?: DEFAULT_PARAMETER_TYPE
if (!parameterType.processTypeIfExtractable(bindingContext, typeParameters, nonDenotableTypes)) continue
val existingParameter = extractedDescriptorToParameter[descriptorToExtract]
val parameter: Parameter =
if (existingParameter != null) {
@@ -431,7 +435,7 @@ private fun ExtractionData.inferParametersInfo(
DescriptorRenderer.HTML.renderType(it)
}.sort().makeString(separator = "\n")
return MaybeError("${JetRefactoringBundle.message("parameter.types.are.not.denotable")!!}\n$typeStr")
return "${JetRefactoringBundle.message("parameter.types.are.not.denotable")!!}\n$typeStr"
}
parameters.addAll(extractedDescriptorToParameter.values())
@@ -443,7 +447,7 @@ private fun ExtractionData.checkLocalDeclarationsWithNonLocalUsages(
allInstructions: List<Instruction>,
localInstructions: List<Instruction>,
bindingContext: BindingContext
): MaybeError<ExtractionDescriptor, String>? {
): String? {
// todo: non-locally used declaration can be turned into the output value
val declarations = ArrayList<JetNamedDeclaration>()
@@ -460,13 +464,13 @@ private fun ExtractionData.checkLocalDeclarationsWithNonLocalUsages(
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 "${JetRefactoringBundle.message("variables.are.used.outside.of.selected.code.fragment")!!}\n$localVarStr"
}
return null
}
private fun ExtractionData.checkDeclarationsMovingOutOfScope(controlFlow: ControlFlow): MaybeError<ExtractionDescriptor, String>? {
private fun ExtractionData.checkDeclarationsMovingOutOfScope(controlFlow: ControlFlow): String? {
val declarationsOutOfScope = HashSet<JetNamedDeclaration>()
if (controlFlow is JumpBasedControlFlow) {
controlFlow.elementToInsertAfterCall.accept(
@@ -483,19 +487,23 @@ private fun ExtractionData.checkDeclarationsMovingOutOfScope(controlFlow: Contro
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 "${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")!!)
fun ExtractionData.performAnalysis(): AnalysisResult {
if (originalElements.empty) {
return AnalysisResult(null, Status.CRITICAL_ERROR, listOf(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")!!)
if (enclosingDeclaration == null) {
return AnalysisResult(null, Status.CRITICAL_ERROR, listOf(JetRefactoringBundle.message("cannot.refactor.no.container")!!))
}
val resolveSession = originalFile.getLazyResolveSession()
val bindingContext = resolveSession.resolveToElement(enclosingDeclaration.getBodyExpression())
@@ -513,17 +521,17 @@ fun ExtractionData.performAnalysis(): Maybe<ExtractionDescriptor, String> {
val parameterError = inferParametersInfo(
commonParent, localInstructions, bindingContext, inferredResultType, replacementMap, parameters, typeParameters
)
if (parameterError != null) return parameterError
if (parameterError != null) {
return AnalysisResult(null, Status.CRITICAL_ERROR, listOf(parameterError))
}
val controlFlowInfo = localInstructions.analyzeControlFlow(bindingContext, parameters, inferredResultType)
if (controlFlowInfo is MaybeError) return MaybeError(controlFlowInfo.error)
val controlFlow = (controlFlowInfo as MaybeValue).value
val messages = ArrayList<String>()
val localDeclError = checkLocalDeclarationsWithNonLocalUsages(pseudocode.getInstructions(), localInstructions, bindingContext)
if (localDeclError != null) return localDeclError
val (controlFlow, controlFlowMessage) = localInstructions.analyzeControlFlow(bindingContext, parameters, inferredResultType)
controlFlowMessage?.let { messages.add(it) }
val outOfScopeError = checkDeclarationsMovingOutOfScope(controlFlow)
if (outOfScopeError != null) return outOfScopeError
checkLocalDeclarationsWithNonLocalUsages(pseudocode.getInstructions(), localInstructions, bindingContext)?.let { messages.add(it) }
checkDeclarationsMovingOutOfScope(controlFlow)?.let { messages.add(it) }
val functionNameValidator = JetNameValidatorImpl(
nextSibling.getParent(),
@@ -536,7 +544,7 @@ fun ExtractionData.performAnalysis(): Maybe<ExtractionDescriptor, String> {
val receiverParameter = if (receiverCandidates.size == 1) receiverCandidates.first() else null
receiverParameter?.let { parameters.remove(it) }
return MaybeValue(
return AnalysisResult(
ExtractionDescriptor(
this,
functionName,
@@ -546,7 +554,9 @@ fun ExtractionData.performAnalysis(): Maybe<ExtractionDescriptor, String> {
typeParameters.sortBy { it.originalDeclaration.getName()!! },
replacementMap,
controlFlow
)
),
if (messages.empty) Status.SUCCESS else Status.NON_CRITICAL_ERROR,
messages
)
}