From 58b9be5c130c5ee1c18ee7bea7d26acfb82a906d Mon Sep 17 00:00:00 2001 From: Alexandr Suhinin Date: Mon, 23 Mar 2020 13:30:39 +0200 Subject: [PATCH] [extract method] kotlin implementation GitOrigin-RevId: 670962d122063309443e919f879f5852915e3af9 --- .../extractMethod/ExtractMethodDialog.java | 2 +- .../extractMethod/ExtractMethodHandler.java | 45 ++- .../extractMethod/newImpl/BodyBuilder.kt | 170 ++++++---- .../extractMethod/newImpl/CallBuilder.kt | 44 ++- .../newImpl/CodeFragmentAnalyzer.kt | 84 ++++- .../newImpl/ExtractMethodAnalyzer.kt | 247 ++++++++++---- .../newImpl/ExtractMethodHelper.kt | 89 ++++- .../newImpl/ExtractOptionsPipeline.kt | 309 ++++++++++++------ .../extractMethod/newImpl/ExtractSelector.kt | 38 ++- .../extractMethod/newImpl/MapFromDialog.kt | 147 +++++++++ .../extractMethod/newImpl/MethodExtractor.kt | 260 +++++++++++---- .../extractMethod/newImpl/SignatureBuilder.kt | 74 ++++- .../newImpl/structures/CodeFragment.java | 73 ----- .../newImpl/structures/DataOutput.kt | 22 +- .../newImpl/structures/ExtractOptions.kt | 12 +- .../newImpl/structures/InputParameter.kt | 6 +- 16 files changed, 1157 insertions(+), 465 deletions(-) create mode 100644 java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MapFromDialog.kt delete mode 100644 java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/CodeFragment.java diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java index fd6017885e4e..682e18edcba7 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java @@ -85,7 +85,7 @@ public class ExtractMethodDialog extends RefactoringDialog implements AbstractEx private TypeSelector mySelector; private final Supplier myDuplicatesCountSupplier; - protected ExtractMethodDialog(Project project, PsiClass targetClass, InputVariables inputVariables, + public ExtractMethodDialog(Project project, PsiClass targetClass, InputVariables inputVariables, PsiType returnType, PsiTypeParameterList typeParameterList, PsiType[] exceptions, boolean isStatic, boolean canBeStatic, boolean canBeChainedConstructor, String title, String helpId, @Nullable Nullability nullability, PsiElement[] elementsToExtract, diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodHandler.java b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodHandler.java index b67448289a40..c733b4898a42 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodHandler.java +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodHandler.java @@ -47,11 +47,7 @@ import com.intellij.refactoring.HelpID; import com.intellij.refactoring.IntroduceTargetChooser; import com.intellij.refactoring.RefactoringActionHandler; import com.intellij.refactoring.RefactoringBundle; -import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodAnalyzerKt; -import com.intellij.refactoring.extractMethod.newImpl.MethodExtractorKt; -import com.intellij.refactoring.extractMethod.newImpl.ExtractSelector; -import com.intellij.refactoring.extractMethod.newImpl.ExtractOptionsPipelineKt; -import com.intellij.refactoring.extractMethod.newImpl.structures.ExtractOptions; +import com.intellij.refactoring.extractMethod.newImpl.*; import com.intellij.refactoring.extractMethod.preview.ExtractMethodPreviewManager; import com.intellij.refactoring.introduceVariable.IntroduceVariableBase; import com.intellij.refactoring.listeners.RefactoringEventData; @@ -156,7 +152,28 @@ public class ExtractMethodHandler implements RefactoringActionHandler, ContextAw return expressions.toArray(PsiElement.EMPTY_ARRAY); } + public static boolean shouldUseNewImpl(@NotNull Project project, PsiFile file, PsiElement @NotNull [] elements){ + if (! Registry.is("java.refactoring.extractMethod.newImplementation")) return false; + final ExtractMethodProcessor processor = getProcessor(project, elements, file, false); + if (processor == null) return true; + try { + processor.prepare(null); + processor.testPrepare(); //TODO remove + processor.myMethodName = "extracted"; + } + catch (PrepareFailedException e) { + return true; + } + + return processor.estimateDuplicatesCount() == 0 && !processor.myInputVariables.isFoldable(); + } + public static void invokeOnElements(@NotNull Project project, final Editor editor, PsiFile file, PsiElement @NotNull [] elements) { + if (shouldUseNewImpl(project, file, elements)) { + new MethodExtractor().doExtract(editor, getRefactoringName(), HelpID.EXTRACT_METHOD); + return; + } + getProcessor(elements, project, file, editor, true, new Pass(){ @Override public void pass(ExtractMethodProcessor processor) { @@ -192,15 +209,6 @@ public class ExtractMethodHandler implements RefactoringActionHandler, ContextAw () -> doRefactoring(project, processor)), getRefactoringName(), null); } - private static void extractWithNewImpl(Editor editor, ExtractMethodProcessor processor){ - final List statements = new ExtractSelector().suggestElementsToExtract(editor); - final ExtractOptions extractOptions = ExtractMethodAnalyzerKt.findExtractOptions(statements); - if (extractOptions == null) return; - final ExtractOptions mappedOptions = ExtractOptionsPipelineKt.remap(extractOptions, processor.myVariableDatum, processor.myMethodName, - processor.myStatic, processor.myMethodVisibility, processor.myIsChainedConstructor); - MethodExtractorKt.extractMethod(mappedOptions); - } - private static void doRefactoring(@NotNull Project project, @NotNull ExtractMethodProcessor processor) { try { final RefactoringEventData beforeData = new RefactoringEventData(); @@ -208,15 +216,6 @@ public class ExtractMethodHandler implements RefactoringActionHandler, ContextAw project.getMessageBus().syncPublisher( RefactoringEventListener.REFACTORING_EVENT_TOPIC).refactoringStarted("refactoring.extract.method", beforeData); - if (Registry.is("java.refactoring.extractMethod.newImplementation") - && processor.estimateDuplicatesCount() == 0 - && ! processor.myInputVariables.isFoldable() - ) { - final Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); - extractWithNewImpl(editor, processor); - return; - } - processor.doRefactoring(); final RefactoringEventData data = new RefactoringEventData(); diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/BodyBuilder.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/BodyBuilder.kt index 3d7ef79cfc8e..a288797cb84e 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/BodyBuilder.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/BodyBuilder.kt @@ -1,16 +1,18 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl -import com.intellij.codeInsight.CodeInsightUtil import com.intellij.psi.* +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiUtil -import com.intellij.refactoring.extractMethod.newImpl.structures.CodeFragment +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.createDeclaration +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.findInCopy +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.findTopmostParenthesis import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput.* import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput.* import com.intellij.refactoring.extractMethod.newImpl.structures.InputParameter -import java.lang.RuntimeException data class PsiReplace(val source: PsiElement, val target: PsiElement) @@ -20,113 +22,137 @@ class BodyBuilder(private val factory: PsiElementFactory) { private fun statementOf(statement: String) = factory.createStatementFromText(statement, null) - private fun findExitSubstitutions(flowOutput: FlowOutput, dataOutput: DataOutput): List { - val replacement = when (flowOutput) { + private fun findDefaultFlowSubstitution(flowOutput: FlowOutput, dataOutput: DataOutput): String? { + return when (flowOutput) { is ConditionalFlow -> when (dataOutput) { is VariableOutput -> "return null;" ArtificialBooleanOutput -> "return true;" is ExpressionOutput -> "return null;" - is EmptyOutput -> throw IllegalArgumentException() + is EmptyOutput -> null } is UnconditionalFlow -> when (dataOutput) { - is VariableOutput, EmptyOutput -> "return;" - is ExpressionOutput -> null - ArtificialBooleanOutput -> throw IllegalArgumentException() + is VariableOutput, is EmptyOutput -> "return;" + is ExpressionOutput, ArtificialBooleanOutput -> null } is EmptyFlow -> null } - return when { - replacement != null -> flowOutput.statements.map { statement -> PsiReplace(statement, statementOf(replacement)) } - else -> emptyList() - } } - private fun createMissedDeclarations(missedDeclarations: List): List { - return missedDeclarations.map { variable -> - factory.createVariableDeclarationStatement(requireNotNull(variable.name), variable.type, null) - } + private fun findExitReplacements(flowOutput: FlowOutput, dataOutput: DataOutput): List { + val replacement = findDefaultFlowSubstitution(flowOutput, dataOutput) ?: return emptyList() + return flowOutput.statements.map { statement -> PsiReplace(statement, statementOf(replacement)) } } private fun createInputReplacements(inputGroup: InputParameter): List { - return inputGroup.references.map { reference -> PsiReplace(reference, expressionOf(inputGroup.name)) } + return inputGroup.references + .map { referenceExpression -> findTopmostParenthesis(referenceExpression) } + .map { normalizedExpression -> PsiReplace(normalizedExpression, expressionOf(inputGroup.name)) } } - private fun getDefaultReturn(dataOutput: DataOutput, flowOutput: FlowOutput): PsiStatement? { - return when(dataOutput) { - is VariableOutput -> statementOf("return ${dataOutput.name};") - is ExpressionOutput -> if (flowOutput is ConditionalFlow) statementOf("return null;") else null - ArtificialBooleanOutput -> statementOf("return false;") - EmptyOutput -> null + private fun findDefaultReturn(dataOutput: DataOutput, flowOutput: FlowOutput): String? { + return when (dataOutput) { + is VariableOutput -> "return ${dataOutput.name};" + is ExpressionOutput -> if (flowOutput is ConditionalFlow) "return null;" else null + ArtificialBooleanOutput -> "return false;" + is EmptyOutput -> null } } - private fun buildBodyForExpression(inputParameters: List, dataOutput: ExpressionOutput): PsiCodeBlock { - val expression = dataOutput.returnExpressions.single() - val normalizedExpression = PsiUtil.skipParenthesizedExprDown(expression)!! - val endStatement = when (dataOutput.type) { - PsiType.VOID -> factory.createStatementFromText("${normalizedExpression.text};", null) - else -> factory.createStatementFromText("return ${normalizedExpression.text};", null) - } - val copyExpression = when (endStatement) { - is PsiReturnStatement -> endStatement.returnValue!! - is PsiExpressionStatement -> endStatement.expression - else -> throw RuntimeException() - } - fun findInCopy(element: T): T { - val sourceStartOffset: Int = normalizedExpression.textRange.startOffset - val copyStartOffset: Int = copyExpression.textRange.startOffset - val range = element.textRange.shiftRight(copyStartOffset - sourceStartOffset) - return CodeInsightUtil.findElementInRange(copyExpression.containingFile, range.startOffset, range.endOffset, element.javaClass) - } - val inCopyParameters = inputParameters.map { parameter -> - InputParameter( - references = parameter.references.map (::findInCopy), - name = parameter.name, - type = parameter.type - ) - } - val replacements = inCopyParameters.map { createInputReplacements(it) }.flatten() - replacements.forEach { replacement -> replacement.source.replace(replacement.target) } + private fun createDeclarationForDisabledParameter(parameter: InputParameter): PsiDeclarationStatement { + val styleManager = CodeStyleManager.getInstance(parameter.references.first().project) + val declaration = factory.createStatementFromText("int ${parameter.name} = ;", null) as PsiDeclarationStatement + val variable = declaration.declaredElements.first() as? PsiVariable + val typeElement = factory.createTypeElement(parameter.type) + variable?.typeElement?.replace(typeElement) + return styleManager.reformat(declaration) as PsiDeclarationStatement + } - val codeBlock = factory.createCodeBlock() - codeBlock.add(endStatement) - return codeBlock + private fun wrapExpression(expression: PsiExpression, shouldBeReturned: Boolean): Pair { + val statement = when (shouldBeReturned) { + false -> factory.createStatementFromText("${expression.text};", null) + true -> factory.createStatementFromText("return ${expression.text};", null) + } + val block = factory.createCodeBlockFromText("{\n}", expression.context) + val addedStatement = block.add(statement) as PsiStatement + val inCopyExpression = when (addedStatement) { + is PsiReturnStatement -> addedStatement.returnValue!! + is PsiExpressionStatement -> addedStatement.expression + else -> throw IllegalStateException() + } + return Pair(addedStatement, inCopyExpression) + } + + private fun findInputParameterInCopy(source: PsiElement, copy: PsiElement, parameter: InputParameter): InputParameter { + return InputParameter( + references = parameter.references.map { reference -> findInCopy(source, copy, reference) }, + name = parameter.name, + type = parameter.type + ) + } + + fun copyOf(elements: List): List { + val block = factory.createCodeBlockFromText("{}", elements.first().context) + block.add(PsiParserFacade.SERVICE.getInstance(elements.first().project).createWhiteSpaceFromText("\n ")) + val first = block.addRange(elements.first(), elements.last()) + val last = block.lastBodyElement!! + return PsiTreeUtil.getElementsOfRange(first, last) } fun build(elements: List, flowOutput: FlowOutput, dataOutput: DataOutput, inputParameters: List, + disabledParameters: List, missedDeclarations: List): PsiCodeBlock { - val expression = elements.singleOrNull() as? PsiExpression - if (expression != null && dataOutput is ExpressionOutput) return buildBodyForExpression(inputParameters, dataOutput) + val project = elements.first().project - val fragment = CodeFragment.of(elements) - val copy = CodeFragment.copyOf(fragment) - val exitCopies = flowOutput.statements.map { statement -> CodeFragment.findSameElementInCopy(fragment, copy, statement) } + val expression = elements.singleOrNull() as? PsiExpression + val normalizedExpression = PsiUtil.skipParenthesizedExprDown(expression) + if (normalizedExpression != null) { + require(dataOutput is ExpressionOutput) + val (wrappedStatement, wrappedExpression) = wrapExpression(normalizedExpression, dataOutput.type != PsiType.VOID) + val wrappedInputParameters = inputParameters.map { parameter -> findInputParameterInCopy(normalizedExpression, wrappedExpression, parameter) } + val wrappedFlowOutput = UnconditionalFlow(listOf(wrappedStatement), true) + val wrappedDataOutput = dataOutput.copy(returnExpressions = listOf(wrappedExpression)) + return build(listOf(wrappedStatement), wrappedFlowOutput, wrappedDataOutput, wrappedInputParameters, disabledParameters, missedDeclarations) + } + + val blockStatement = elements.singleOrNull() as? PsiBlockStatement + val firstElement = blockStatement?.codeBlock?.firstBodyElement ?: elements.first() + val lastElement = blockStatement?.codeBlock?.lastBodyElement ?: elements.last() + val normalizedElements = PsiTreeUtil.getElementsOfRange(firstElement, lastElement) + .dropWhile { it is PsiWhiteSpace } + .dropLastWhile { it is PsiWhiteSpace } + + val copy = copyOf(normalizedElements) + + val exitCopies = flowOutput.statements.map { statement -> findInCopy(normalizedElements.first(), copy.first(), statement) } val inCopyFlowOutput = when (flowOutput) { is ConditionalFlow -> ConditionalFlow(exitCopies) is UnconditionalFlow -> UnconditionalFlow(exitCopies, flowOutput.isDefaultExit) EmptyFlow -> EmptyFlow } - val inCopyInputGroups = inputParameters.map { group -> - InputParameter( - references = group.references.map { statement -> CodeFragment.findSameElementInCopy(fragment, copy, statement) }, - name = group.name, - type = group.type - ) - } - val exitSubstitution = findExitSubstitutions(inCopyFlowOutput, dataOutput) - val defaultReturn = getDefaultReturn(dataOutput, flowOutput) + val inCopyInputGroups = inputParameters.map { parameter -> findInputParameterInCopy(normalizedElements.first(), copy.first(), parameter) } + val exitSubstitution = findExitReplacements(inCopyFlowOutput, dataOutput) val inputReplacements = inCopyInputGroups.map { createInputReplacements(it) }.flatten() - val requiredDeclarations = createMissedDeclarations(missedDeclarations) + val requiredDeclarations = missedDeclarations.map { createDeclaration(it) } (inputReplacements + exitSubstitution).forEach { (source, target) -> source.replace(target) } + + val block = copy.first().parent as PsiCodeBlock + + val defaultReturn = findDefaultReturn(dataOutput, flowOutput) if (defaultReturn != null) { - copy.commonParent.addAfter(defaultReturn, copy.lastElement) + block.addAfter(statementOf(defaultReturn), copy.last()) } - requiredDeclarations.forEach { declaration -> copy.commonParent.addBefore(declaration, copy.firstElement) } - return copy.commonParent as PsiCodeBlock + val disabledDeclarations = disabledParameters.map { createDeclarationForDisabledParameter(it) } + disabledDeclarations.reversed().forEach { declaration -> + block.addBefore(declaration, copy.first()) + val newLine = PsiParserFacade.SERVICE.getInstance(project).createWhiteSpaceFromText("\n") + block.addBefore(newLine, copy.first()) + } + requiredDeclarations.forEach { declaration -> block.addBefore(declaration, copy.first()) } + return block } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CallBuilder.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CallBuilder.kt index 598221ef0eb4..c260c3bb0272 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CallBuilder.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CallBuilder.kt @@ -1,32 +1,41 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl +import com.intellij.openapi.project.Project import com.intellij.psi.* +import com.intellij.psi.util.PsiUtil +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.createDeclaration import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput.* import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput.* -import java.lang.IllegalArgumentException -import java.lang.IllegalStateException -class CallFactory(private val factory: PsiElementFactory) { +class CallBuilder(project: Project, private val context: PsiElement?) { - private fun expressionOf(expression: String) = factory.createExpressionFromText(expression, null) + private val factory: PsiElementFactory = PsiElementFactory.getInstance(project) - private fun statementsOf(vararg statements: String) = statements.map { statement -> factory.createStatementFromText(statement, null) } + private fun expressionOf(expression: String) = factory.createExpressionFromText(expression, context) + + private fun statementsOf(vararg statements: String) = statements.map { statement -> factory.createStatementFromText(statement, context) } private fun createDeclaration(type: PsiType?, name: String, initializer: String): PsiStatement { return when { type != null -> factory.createVariableDeclarationStatement(name, type, expressionOf(initializer)) - else -> factory.createStatementFromText("$name = $initializer;", null) + else -> factory.createStatementFromText("$name = $initializer;", context) } } private fun variableDeclaration(methodCall: String, dataOutput: DataOutput): List { val declaration = when (dataOutput) { is VariableOutput -> createDeclaration(dataOutput.type.takeIf { dataOutput.declareType }, dataOutput.name, methodCall) - is ExpressionOutput -> createDeclaration(dataOutput.type, dataOutput.name, methodCall) - ArtificialBooleanOutput, EmptyOutput -> null + is ExpressionOutput -> createDeclaration(dataOutput.type, dataOutput.name!!, methodCall) + ArtificialBooleanOutput, is EmptyOutput -> null + } + val declarationStatement = declaration as? PsiDeclarationStatement + val declaredVariable = declarationStatement?.declaredElements?.firstOrNull() as? PsiVariable + if (dataOutput is VariableOutput && declaredVariable != null) { + val needsFinal = dataOutput.variable.hasModifierProperty(PsiModifier.FINAL) + PsiUtil.setModifierProperty(declaredVariable, PsiModifier.FINAL, needsFinal) } return listOfNotNull(declaration) } @@ -36,7 +45,7 @@ class CallFactory(private val factory: PsiElementFactory) { is VariableOutput -> "if (${dataOutput.name} == null) ${flow.statements.first().text}" is ExpressionOutput -> "if (${dataOutput.name} != null) return ${dataOutput.name};" ArtificialBooleanOutput -> "if ($methodCall) ${flow.statements.first().text}" - EmptyOutput -> throw IllegalArgumentException() + is EmptyOutput -> throw IllegalArgumentException() } return statementsOf(exit) } @@ -50,7 +59,7 @@ class CallFactory(private val factory: PsiElementFactory) { "return $methodCall;" ) ArtificialBooleanOutput -> throw IllegalStateException() - EmptyOutput -> when { + is EmptyOutput -> when { flow.isDefaultExit -> statementsOf("$methodCall;") else -> statementsOf( "$methodCall;", @@ -68,12 +77,17 @@ class CallFactory(private val factory: PsiElementFactory) { } } - private fun declarationsOf(variables: List): List { - return variables.map { variable -> factory.createVariableDeclarationStatement(requireNotNull(variable.name), variable.type, null) } - } - fun buildCall(methodCall: String, flowOutput: FlowOutput, dataOutput: DataOutput, exposedDeclarations: List): List { val variableDeclaration = if (flowOutput !is ConditionalFlow && dataOutput is ExpressionOutput) emptyList() else variableDeclaration(methodCall, dataOutput) - return variableDeclaration + createFlowStatements(methodCall, flowOutput, dataOutput) + declarationsOf(exposedDeclarations) + return variableDeclaration + createFlowStatements(methodCall, flowOutput, dataOutput) + exposedDeclarations.map { createDeclaration(it) } + } + + fun buildExpressionCall(methodCall: String, dataOutput: DataOutput): List { + require(dataOutput is ExpressionOutput) + val expression = when { + dataOutput.name != null -> "${dataOutput.name} = $methodCall" + else -> methodCall + } + return listOf(factory.createExpressionFromText(expression, context)) } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CodeFragmentAnalyzer.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CodeFragmentAnalyzer.kt index 2a0dd2522124..6224fadb36f2 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CodeFragmentAnalyzer.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/CodeFragmentAnalyzer.kt @@ -1,20 +1,32 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl +import com.intellij.codeInsight.CodeInsightUtil import com.intellij.codeInsight.ExceptionUtil import com.intellij.codeInsight.Nullability +import com.intellij.codeInsight.generation.GenerateMembersUtil import com.intellij.codeInspection.dataFlow.* import com.intellij.codeInspection.dataFlow.value.DfaValue +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.TextRange import com.intellij.psi.* +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.codeStyle.VariableKind import com.intellij.psi.controlFlow.* import com.intellij.psi.controlFlow.ControlFlow import com.intellij.psi.controlFlow.ControlFlowUtil.DEFAULT_EXIT_STATEMENTS_CLASSES +import com.intellij.psi.impl.source.codeStyle.JavaCodeStyleManagerImpl import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiUtil +import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput +import com.intellij.refactoring.extractMethod.newImpl.structures.ExtractOptions import com.intellij.refactoring.util.classMembers.ClassMemberReferencesVisitor import com.intellij.util.containers.IntArrayList import com.siyeh.ig.psiutils.VariableAccessUtils +import java.util.LinkedHashSet +import kotlin.Comparator +import kotlin.collections.ArrayList +import kotlin.collections.HashSet data class ExitDescription(val statements: List, val numberOfExits: Int, val hasSpecialExits: Boolean) data class ExternalReference(val variable: PsiVariable, val references: List) @@ -25,13 +37,12 @@ class CodeFragmentAnalyzer(val elements: List) { init { require(elements.isNotEmpty()) } - + private val codeFragment = ControlFlowUtil.findCodeFragment(elements.first()) private val flow: ControlFlow = createControlFlow(elements) - private val flowRange = findFlowRange(flow, elements) private fun createControlFlow(elements: List): ControlFlow { - val fragmentToAnalyze: PsiElement = ControlFlowUtil.findCodeFragment(elements.first()) + val fragmentToAnalyze: PsiElement = codeFragment val flowPolicy = LocalsControlFlowPolicy(fragmentToAnalyze) val factory: ControlFlowFactory = ControlFlowFactory.getInstance(elements.first().project) return factory.getControlFlow(fragmentToAnalyze, flowPolicy, false, false) @@ -54,9 +65,26 @@ class CodeFragmentAnalyzer(val elements: List) { fun findExternalReferences(): List { return ControlFlowUtil.getInputVariables(flow, flowRange.first, flowRange.last) .filterNot { variable -> variable in this } + .sortedWith( Comparator { v1: PsiVariable, v2: PsiVariable -> when { + v1.type is PsiEllipsisType -> 1 + v2.type is PsiEllipsisType -> -1 + else -> v1.textOffset - v2.textOffset + }}) .map { variable -> ExternalReference(variable, findVariableReferences(variable)) } } + fun findUsedVariablesAfter(): List { + return ControlFlowUtil.getUsedVariables(flow, flowRange.last, flow.size) + } + + fun findOuterLocals(sourceClassMember: PsiElement, targetClassMember: PsiElement): List? { + val outerVariables = mutableListOf() + val canBeExtracted = elements + .all { element -> ControlFlowUtil.collectOuterLocals(outerVariables, element, sourceClassMember, targetClassMember) } + if (!canBeExtracted) return null + return outerVariables.map { variable -> ExternalReference(variable, findVariableReferences(variable)) } + } + fun findOutputVariables(): List { val exitPoints = IntArrayList() ControlFlowUtil.findExitPointsAndStatements(flow, flowRange.first, flowRange.last, exitPoints, *DEFAULT_EXIT_STATEMENTS_CLASSES) @@ -71,6 +99,10 @@ class CodeFragmentAnalyzer(val elements: List) { } } + fun hasObservableThrowExit(): Boolean { + return ControlFlowUtil.hasObservableThrowExitPoints(flow, flowRange.first, flowRange.last, elements.toTypedArray(), codeFragment) + } + fun findExitDescription(): ExitDescription { val statements = ControlFlowUtil .findExitPointsAndStatements(flow, flowRange.first, flowRange.last, IntArrayList(), *DEFAULT_EXIT_STATEMENTS_CLASSES) @@ -141,7 +173,8 @@ class CodeFragmentAnalyzer(val elements: List) { else -> listOf(flowRange.last) } return defaultExits.filterNot { it in flowRange.first until flowRange.last } - } else { + } + else { return emptyList() } } @@ -169,12 +202,14 @@ class CodeFragmentAnalyzer(val elements: List) { } fun findExposedLocalVariables(expressions: List): List { - val writtenVariables = ControlFlowUtil.getWrittenVariables(flow, flowRange.first, flowRange.last, false) val exposedLocalVariables = HashSet() val visitor = object : JavaRecursiveElementWalkingVisitor() { override fun visitReferenceExpression(reference: PsiReferenceExpression) { - exposedLocalVariables.addAll(writtenVariables.filter { variable: PsiVariable -> reference.isReferenceTo(variable) }) + val variable = reference.resolve() as? PsiVariable ?: return + if (variable.textRange in TextRange(elements.first().textRange.startOffset, elements.last().textRange.endOffset)) { + exposedLocalVariables += variable + } } } expressions.forEach { it.accept(visitor) } @@ -203,6 +238,8 @@ class CodeFragmentAnalyzer(val elements: List) { companion object { fun inferNullability(expressionGroup: List): Nullability { + if (expressionGroup.any { it.text == "null" }) return Nullability.NULLABLE + if (expressionGroup.isEmpty()) return Nullability.UNKNOWN val fragmentToAnalyze = ControlFlowUtil.findCodeFragment(expressionGroup.first()) val dfaRunner = DataFlowRunner(fragmentToAnalyze.project) @@ -230,17 +267,30 @@ class CodeFragmentAnalyzer(val elements: List) { } } - fun inferNullability(scopeElements: List, expression: String?): Nullability { - if (expression == null) return Nullability.UNKNOWN - val factory = PsiElementFactory.getInstance(scopeElements.first().project) - val analyzer = CodeFragmentAnalyzer(scopeElements) - val codeBlock = factory.createCodeBlock() - val inputVariables = analyzer.findUndeclaredVariables() - val virtualReturn = factory.createStatementFromText("return $expression;", null) - inputVariables.forEach { codeBlock.add(it) } - codeBlock.addRange(scopeElements.first(), scopeElements.last()) - val probeExpression = (codeBlock.add(virtualReturn) as PsiReturnStatement).returnValue ?: return Nullability.UNKNOWN - return inferNullability(listOf(probeExpression)) + fun inferNullability(place: PsiStatement, probeExpression: String?): Nullability { + if (probeExpression == null) return Nullability.UNKNOWN + val factory = PsiElementFactory.getInstance(place.project) + val sourceClass = findClassMember(place)?.containingClass ?: return Nullability.UNKNOWN + val copyClass = sourceClass.copy() + val copyPlace = findSameElementInCopy(sourceClass, copyClass, place) + val probeStatement = factory.createStatementFromText("return $probeExpression;", null) + + val parent = copyPlace.parent + val codeBlock = when(parent) { + is PsiCodeBlock -> copyPlace.parent as PsiCodeBlock + else -> (copyPlace.parent.replace(factory.createCodeBlock()) as PsiCodeBlock) + .also { codeBlock -> codeBlock.add(copyPlace) } + } + val artificialReturn = codeBlock.add(probeStatement) as PsiReturnStatement + val artificialExpression = requireNotNull(artificialReturn.returnValue) + return inferNullability(listOf(artificialExpression)) + } + + private fun findSameElementInCopy(source: PsiElement, copy: PsiElement, element: T): T { + val sourceStartOffset = source.textRange.startOffset + val copyStartOffset = copy.textRange.startOffset + val range = element.textRange.shiftRight(copyStartOffset - sourceStartOffset) + return CodeInsightUtil.findElementInRange(copy.containingFile, range.startOffset, range.endOffset, element.javaClass) } fun findReturnExpressionsIn(scope: PsiElement): List { diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodAnalyzer.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodAnalyzer.kt index cc16bf178e2d..4582d0fbf8b7 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodAnalyzer.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodAnalyzer.kt @@ -1,10 +1,23 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl +import com.intellij.codeInsight.AnnotationUtil import com.intellij.codeInsight.Nullability +import com.intellij.codeInsight.NullableNotNullManager +import com.intellij.openapi.util.TextRange import com.intellij.psi.* +import com.intellij.psi.search.searches.ReferencesSearch import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiTypesUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.refactoring.extractMethod.PrepareFailedException +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.findUsedTypeParameters +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.getExpressionType +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.guessName import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.hasExplicitModifier +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.inputParameterOf +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.normalizedAnchor +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.uniqueNameOf import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.withBoxedType import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput.* @@ -12,54 +25,47 @@ import com.intellij.refactoring.extractMethod.newImpl.structures.ExtractOptions import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput.* import com.intellij.refactoring.extractMethod.newImpl.structures.InputParameter -import com.intellij.refactoring.util.RefactoringUtil +import com.intellij.util.Processor +import java.util.* -fun findExtractOptions(elements: List): ExtractOptions? { +fun findExtractOptions(elements: List): ExtractOptions { + require(elements.isNotEmpty()) val analyzer = CodeFragmentAnalyzer(elements) - val flowOutput = findFlowOutput(analyzer) ?: return null + val flowOutput = findFlowOutput(analyzer) ?: throw PrepareFailedException("too many exits", elements.first()) - val variableData = findVariableData(analyzer, analyzer.findOutputVariables()) ?: return null + val variableData = findVariableData(analyzer, analyzer.findOutputVariables()) val expression = elements.singleOrNull() as? PsiExpression + + //TODO use correct error messages val dataOutput = when { - expression != null -> ExpressionOutput(ExtractMethodHelper.getExpressionType(expression), "x", listOf(expression)) - variableData != EmptyOutput -> when { - ExtractMethodHelper.areSame(flowOutput.statements) -> variableData - else -> return null + expression != null -> ExpressionOutput(getExpressionType(expression), null, listOf(expression), CodeFragmentAnalyzer.inferNullability(listOf(expression))) + variableData is VariableOutput -> when { + ! ExtractMethodHelper.areSame(flowOutput.statements) && flowOutput is ConditionalFlow -> throw PrepareFailedException("Out var and different flow statements", flowOutput.statements.first()) + variableData.nullability != Nullability.NOT_NULL && flowOutput is ConditionalFlow -> throw PrepareFailedException("Nullable out var and branching", variableData.variable) + flowOutput is ConditionalFlow -> variableData.copy(nullability = Nullability.NULLABLE) + else -> variableData } - else -> findFlowData(analyzer, flowOutput) ?: return null + else -> findFlowData(analyzer, flowOutput) } - val boxedDataOutput = when (flowOutput) { - is ConditionalFlow -> dataOutput.withBoxedType() - else -> dataOutput - } + val anchor = findClassMember(elements.first()) ?: throw PrepareFailedException("No upper class", elements.first()) - if (flowOutput is ConditionalFlow && !isNotNullData(analyzer, dataOutput)) return null + val typeParameters = findUsedTypeParameters((anchor as? PsiTypeParameterListOwner)?.typeParameterList, elements) - val anchor = findDefaultAnchor(elements.first()) ?: return null - - fun findUsedTypeParameters(source: PsiTypeParameterList?, searchScope: List): List { - return RefactoringUtil - .createTypeParameterListWithUsedTypeParameters(source, *searchScope.toTypedArray())?.typeParameters.orEmpty().toList() - } - - val typeParameters = PsiElementFactory.getInstance(elements.first().project).createTypeParameterList() - if (anchor is PsiMethod){ - val classTypeParameters = findUsedTypeParameters(anchor.containingClass?.typeParameterList, elements) - val methodTypeParameters = findUsedTypeParameters(anchor.typeParameterList, elements) - (classTypeParameters + methodTypeParameters).forEach { typeParameters.add(it) } - } - - val inputParameters = analyzer.findExternalReferences().map { externalReference -> inputParameterOf(externalReference) } + val inputParameters = analyzer.findExternalReferences() + .map { externalReference -> inputParameterOf(externalReference) } + .map { it.copy(type = normalizeType(it.type)) } val parameterNames = inputParameters.map { it.name }.toSet() - val extractOptions = ExtractOptions( + val exposedVariables = analyzer.findExposedLocalDeclarations() + + var extractOptions = ExtractOptions( anchor = anchor, elements = elements, flowOutput = flowOutput, - dataOutput = boxedDataOutput, + dataOutput = normalizeDataOutput(dataOutput, flowOutput, elements, exposedVariables.mapNotNull { it.name }), thrownExceptions = analyzer.findThrownExceptions(), requiredVariablesInside = analyzer.findUndeclaredVariables().filterNot { it.name in parameterNames }, typeParameters = typeParameters, @@ -67,74 +73,187 @@ fun findExtractOptions(elements: List): ExtractOptions? { isConstructor = false, isStatic = false, visibility = "private", - inputParameters = analyzer.findExternalReferences().map { externalReference -> inputParameterOf(externalReference) }, - exposedLocalVariables = analyzer.findExposedLocalDeclarations() + inputParameters = inputParameters, + disabledParameters = emptyList(), + exposedLocalVariables = exposedVariables ) + extractOptions = ExtractMethodPipeline.withCastedParameters(extractOptions) + val targetClass = PsiTreeUtil.getParentOfType(ExtractMethodHelper.getValidParentOf(elements.first()), PsiClass::class.java)!! val fieldUsages = analyzer.findFieldUsages(targetClass, elements) - if (!extractOptions.isConstructor && fieldUsages.any { it.isWrite && it.field.hasExplicitModifier("final") }) { - return null + val finalFields = fieldUsages.filter { it.isWrite && it.field.hasExplicitModifier("final") }.map { it.field }.distinct() + extractOptions = when (finalFields.size) { + 0 -> extractOptions + 1 -> when (extractOptions.dataOutput is EmptyOutput) { + true -> extractOptions.copy( + dataOutput = VariableOutput(finalFields.first().type, finalFields.first(), false), + requiredVariablesInside = listOf(finalFields.first()) + ) + false -> throw PrepareFailedException("Too many final fields", finalFields.first()) + } + else -> throw PrepareFailedException("Too many final fields", finalFields.first()) } - return extractOptions.copy(inputParameters = extractOptions.inputParameters).withDefaultStatic() + checkLocalClass(extractOptions) + + return ExtractMethodPipeline.withDefaultStatic(extractOptions) } -private fun findDefaultAnchor(element: PsiElement): PsiMember? { +private fun normalizeDataOutput(dataOutput: DataOutput, flowOutput: FlowOutput, elements: List, reservedNames: List): DataOutput { + val boxedDataOutput = when (flowOutput) { + is ConditionalFlow -> dataOutput.withBoxedType() + else -> dataOutput + } + val uniqueName = when (boxedDataOutput) { + is ExpressionOutput -> boxedDataOutput.copy(name = uniqueNameOf(boxedDataOutput.name, elements, reservedNames)) + else -> boxedDataOutput + } + return uniqueName +} + +private fun normalizeType(type: PsiType): PsiType { + return when (type) { + is PsiDisjunctionType -> PsiTypesUtil.getLowestUpperBoundClassType(type)!! + else -> GenericsUtil.getVariableTypeByExpressionType(type) + } +} + +fun findClassMember(element: PsiElement): PsiMember? { val holderTypes = arrayOf(PsiMethod::class.java, PsiField::class.java, PsiClassInitializer::class.java) - return PsiTreeUtil.getNonStrictParentOfType(ExtractMethodHelper.getValidParentOf(element), *holderTypes) + val anchor = PsiTreeUtil.getNonStrictParentOfType(ExtractMethodHelper.getValidParentOf(element), *holderTypes) ?: return null + return normalizedAnchor(anchor) } private fun findFlowOutput(analyzer: CodeFragmentAnalyzer): FlowOutput? { + if (analyzer.hasObservableThrowExit()) return null val (exitStatements, numberOfExits, hasSpecialExits) = analyzer.findExitDescription() return when (numberOfExits) { 1 -> if (exitStatements.isNotEmpty()) UnconditionalFlow(exitStatements, !hasSpecialExits) else EmptyFlow - 2 -> ConditionalFlow(exitStatements) + 2 -> if (exitStatements.isNotEmpty()) ConditionalFlow(exitStatements) else null else -> return null } } -private fun inputParameterOf(externalReference: ExternalReference) = with(externalReference) { - InputParameter(references, requireNotNull(variable.name), variable.type) -} - -private fun findFlowData(analyzer: CodeFragmentAnalyzer, flowOutput: FlowOutput): DataOutput? { +private fun findOutputFromReturn(flowOutput: FlowOutput): ExpressionOutput? { val returnExpressions = flowOutput.statements.mapNotNull { statement -> (statement as? PsiReturnStatement)?.returnValue } val returnType = returnExpressions.asSequence().mapNotNull { expression -> expression.type }.firstOrNull() - val variableName = returnExpressions.asSequence().map { expression -> ExtractMethodHelper.guessName(expression) }.firstOrNull() ?: "out" - val returnOutput = if (returnType != null) ExpressionOutput(returnType, variableName, returnExpressions) else null + val variableName = returnExpressions.asSequence().map { expression -> guessName(expression) }.firstOrNull() ?: "x" + val nullability = CodeFragmentAnalyzer.inferNullability(returnExpressions) + return if (returnType != null) ExpressionOutput(returnType, variableName, returnExpressions, nullability) else null +} + +private fun findFlowData(analyzer: CodeFragmentAnalyzer, flowOutput: FlowOutput): DataOutput { + val returnOutput = findOutputFromReturn(flowOutput) return when (flowOutput) { is ConditionalFlow -> when { - ExtractMethodHelper.areSame( - flowOutput.statements) && analyzer.findExposedLocalVariables(returnExpressions).isEmpty() -> ArtificialBooleanOutput - CodeFragmentAnalyzer.inferNullability(returnExpressions) == Nullability.NOT_NULL -> returnOutput ?: return null - else -> null + returnOutput?.nullability == Nullability.NOT_NULL && returnOutput.type != PsiType.BOOLEAN -> returnOutput.copy(nullability = Nullability.NULLABLE) + ExtractMethodHelper.areSame(flowOutput.statements) && analyzer.findExposedLocalVariables(returnOutput?.returnExpressions.orEmpty()).isEmpty() -> + ArtificialBooleanOutput + else -> throw PrepareFailedException("Nullable output and branching", analyzer.elements.first()) } - is UnconditionalFlow -> returnOutput ?: EmptyOutput - EmptyFlow -> EmptyOutput + is UnconditionalFlow -> returnOutput ?: EmptyOutput() + EmptyFlow -> EmptyOutput() } } -private fun findVariableData(analyzer: CodeFragmentAnalyzer, variables: List): DataOutput? { - return when { - variables.isEmpty() -> EmptyOutput - variables.size == 1 -> VariableOutput(variables.single().type, variables.single(), variables.single() in analyzer) - else -> null +//TODO correct messages in PrepareFailedException +private fun findVariableData(analyzer: CodeFragmentAnalyzer, variables: List): DataOutput { + val variable = when { + analyzer.elements.singleOrNull() is PsiExpression && variables.isNotEmpty() -> throw PrepareFailedException("Var in expression", variables.first()) + variables.isEmpty() -> return EmptyOutput() + variables.size > 1 -> throw PrepareFailedException("Many vars", variables[1]) + else -> variables.single() } + val nullability = CodeFragmentAnalyzer.inferNullability(analyzer.elements.last() as PsiStatement, variable.name) + return VariableOutput(variables.single().type, variables.single(), variables.single() in analyzer, nullability) } -private fun isNotNullData(analyzer: CodeFragmentAnalyzer, dataOutput: DataOutput): Boolean { - return when (dataOutput) { - is VariableOutput -> CodeFragmentAnalyzer.inferNullability(analyzer.elements, dataOutput.variable.name) == Nullability.NOT_NULL - is ExpressionOutput -> CodeFragmentAnalyzer.inferNullability(dataOutput.returnExpressions) == Nullability.NOT_NULL - is ArtificialBooleanOutput, is EmptyOutput -> true - } +private fun PsiModifierListOwner?.hasNullabilityAnnotation(): Boolean { + if (this == null) return false + val nullabilityManager = NullableNotNullManager.getInstance(project) + val nullabilityAnnotations = nullabilityManager.notNulls + nullabilityManager.nullables + return AnnotationUtil.isAnnotated(this, nullabilityAnnotations, AnnotationUtil.CHECK_TYPE) } -internal fun updateMethodAnnotations(method: PsiMethod) { +internal fun updateMethodAnnotations(method: PsiMethod,inputParameters: List) { if (method.returnType !is PsiPrimitiveType) { + //TODO use dataoutput.nullability instead val resultNullability = CodeFragmentAnalyzer.inferNullability(CodeFragmentAnalyzer.findReturnExpressionsIn(method)) ExtractMethodHelper.addNullabilityAnnotation(method, resultNullability) } + val parameters = method.parameterList.parameters + inputParameters + .filter { ((it.references.first() as? PsiReferenceExpression)?.resolve() as? PsiModifierListOwner).hasNullabilityAnnotation() } + .forEach { inputParameter -> + val parameterNullability = CodeFragmentAnalyzer.inferNullability(inputParameter.references) + val parameter = parameters.find { it.name == inputParameter.name } + if (parameter != null) ExtractMethodHelper.addNullabilityAnnotation(parameter, parameterNullability) + } +} + +private fun checkLocalClass(options: ExtractOptions): Boolean { + var container: PsiElement? = PsiTreeUtil.getParentOfType(options.elements.first(), PsiClass::class.java, PsiMethod::class.java) + while (container is PsiMethod && container.containingClass !== options.anchor.parent) { + container = PsiTreeUtil.getParentOfType(container, PsiMethod::class.java, true) + } + container ?: return true + val analyzer = CodeFragmentAnalyzer(options.elements) + val localClasses = mutableListOf() + container.accept(object : JavaRecursiveElementWalkingVisitor() { + override fun visitClass(aClass: PsiClass) { + localClasses.add(aClass) + } + + override fun visitAnonymousClass(aClass: PsiAnonymousClass) { + visitElement(aClass) + } + + override fun visitTypeParameter(classParameter: PsiTypeParameter) { + visitElement(classParameter) + } + }) + fun isExtracted(element: PsiElement): Boolean { + return element.textRange in TextRange(options.elements.first().textRange.startOffset, options.elements.last().textRange.endOffset) + } + for (localClass in localClasses) { + val classExtracted: Boolean = isExtracted(localClass) + val extractedReferences = Collections.synchronizedList(ArrayList()) + val remainingReferences = Collections.synchronizedList(ArrayList()) + ReferencesSearch.search(localClass).forEach(Processor { psiReference: PsiReference -> + val element = psiReference.element + val elementExtracted: Boolean = isExtracted(element) + if (elementExtracted && !classExtracted) { + extractedReferences.add(element) + return@Processor false + } + if (!elementExtracted && classExtracted) { + remainingReferences.add(element) + return@Processor false + } + true + }) + if (extractedReferences.isNotEmpty()) { + throw PrepareFailedException( + "Cannot extract method because the selected code fragment uses local classes defined outside of the fragment", + extractedReferences[0]) + } + if (remainingReferences.isNotEmpty()) { + throw PrepareFailedException( + "Cannot extract method because the selected code fragment defines local classes used outside of the fragment", + remainingReferences[0]) + } + if (classExtracted) { + analyzer.findUsedVariablesAfter() + .filter { isExtracted(it) } + .filter { PsiUtil.resolveClassInType(it.type) === localClass } + .forEach { + throw PrepareFailedException( + "Cannot extract method because the selected code fragment defines variable of local class type used outside of the fragment", it + ) + } + } + } + return true } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodHelper.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodHelper.kt index 374feec1bdac..d378c77d2f4f 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodHelper.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractMethodHelper.kt @@ -1,29 +1,42 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl +import com.intellij.codeInsight.CodeInsightUtil import com.intellij.codeInsight.Nullability import com.intellij.codeInsight.NullableNotNullManager import com.intellij.codeInsight.PsiEquivalenceUtil import com.intellij.codeInsight.intention.AddAnnotationPsiFix +import com.intellij.openapi.util.TextRange import com.intellij.psi.* import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.psi.impl.source.DummyHolder import com.intellij.psi.impl.source.codeStyle.JavaCodeStyleManagerImpl import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput.* +import com.intellij.refactoring.extractMethod.newImpl.structures.InputParameter import com.intellij.refactoring.util.RefactoringUtil object ExtractMethodHelper { - fun wrapWithCodeBlock(elements: List): List { + fun wrapWithCodeBlock(elements: List): List { require(elements.isNotEmpty()) val codeBlock = PsiElementFactory.getInstance(elements.first().project).createCodeBlock() elements.forEach { codeBlock.add(it) } return listOf(codeBlock) } + fun findUsedTypeParameters(source: PsiTypeParameterList?, searchScope: List): List { + val typeParameterList = RefactoringUtil.createTypeParameterListWithUsedTypeParameters(source, *searchScope.toTypedArray()) + return typeParameterList?.typeParameters.orEmpty().toList() + } + + fun inputParameterOf(externalReference: ExternalReference) = with(externalReference) { + InputParameter(references, requireNotNull(variable.name), variable.type) + } + fun PsiElement.addSiblingAfter(element: PsiElement): PsiElement { return this.parent.addAfter(element, this) } @@ -38,6 +51,21 @@ object ExtractMethodHelper { return physicalParent ?: throw IllegalArgumentException() } + fun normalizedAnchor(anchor: PsiMember): PsiMember { + return when (anchor) { + is PsiField -> findLastFieldInDeclaration(anchor) + else -> anchor + } + } + + private fun findLastFieldInDeclaration(field: PsiField): PsiField { + val nextSibling = PsiTreeUtil.skipWhitespacesForward(field) + return when (PsiUtil.getElementType(nextSibling)) { + JavaTokenType.COMMA -> PsiTreeUtil.skipWhitespacesForward(nextSibling) as PsiField + else -> field + } + } + fun addNullabilityAnnotation(owner: PsiModifierListOwner, nullability: Nullability) { val nullabilityManager = NullableNotNullManager.getInstance(owner.project) val annotation = when (nullability) { @@ -50,14 +78,61 @@ object ExtractMethodHelper { JavaCodeStyleManager.getInstance(owner.project).shortenClassReferences(annotationElement) } + private fun findVariableReferences(element: PsiElement): Sequence { + val references = PsiTreeUtil.findChildrenOfAnyType(element, PsiReferenceExpression::class.java) + return references.asSequence().mapNotNull { reference -> (reference.resolve() as? PsiVariable) } + } + + fun findInCopy(firstInSource: PsiElement, firstInCopy: PsiElement, element: T): T { + val sourceStartOffset: Int = firstInSource.textRange.startOffset + val copyStartOffset: Int = firstInCopy.textRange.startOffset + val range = element.textRange.shiftRight(copyStartOffset - sourceStartOffset) + return CodeInsightUtil.findElementInRange(firstInCopy.containingFile, range.startOffset, range.endOffset, element.javaClass) + } + + fun hasConflictResolve(name: String?, scopeToIgnore: List): Boolean { + require(scopeToIgnore.isNotEmpty()) + if (name == null) return false + val lastElement = scopeToIgnore.last() + val helper = JavaPsiFacade.getInstance(lastElement.project).resolveHelper + val resolvedRange = helper.resolveAccessibleReferencedVariable(name, lastElement.context)?.textRange ?: return false + return resolvedRange !in TextRange(scopeToIgnore.first().textRange.startOffset, scopeToIgnore.last().textRange.endOffset) + } + + fun uniqueNameOf(name: String?, scopeToIgnore: List, reservedNames: List): String? { + require(scopeToIgnore.isNotEmpty()) + if (name == null) return null + val lastElement = scopeToIgnore.last() + if (hasConflictResolve(name, scopeToIgnore) || name in reservedNames){ + val styleManager = JavaCodeStyleManager.getInstance(lastElement.project) as JavaCodeStyleManagerImpl + return styleManager.suggestUniqueVariableName(name, lastElement, true) + } else { + return name + } + } + fun guessName(expression: PsiExpression): String? { val codeStyleManager = JavaCodeStyleManager.getInstance(expression.project) as JavaCodeStyleManagerImpl - val name = codeStyleManager - .suggestSemanticNames(expression).firstOrNull() - ?: PsiTreeUtil.findChildOfType(expression, PsiReferenceExpression::class.java)?.referenceName - ?: "x" - return codeStyleManager.suggestUniqueVariableName(name, expression, true) + return findVariableReferences(expression).mapNotNull { variable -> variable.name }.firstOrNull() + ?: codeStyleManager.suggestSemanticNames(expression).firstOrNull() + ?: "x" + } + + fun createDeclaration(variable: PsiVariable): PsiDeclarationStatement { + val factory = PsiElementFactory.getInstance(variable.project) + val declaration = factory.createVariableDeclarationStatement(requireNotNull(variable.name), variable.type, null) + val declaredVariable = declaration.declaredElements.first() as PsiVariable + PsiUtil.setModifierProperty(declaredVariable, PsiModifier.FINAL, variable.hasModifierProperty(PsiModifier.FINAL)) + return declaration + } + + tailrec fun findTopmostParenthesis(expression: PsiExpression): PsiExpression { + val parent = expression.parent as? PsiParenthesizedExpression + return when (parent?.expression) { + expression -> findTopmostParenthesis(parent) + else -> expression + } } fun getExpressionType(expression: PsiExpression): PsiType { @@ -94,7 +169,7 @@ object ExtractMethodHelper { return when (this) { is VariableOutput -> copy(type = boxedTypeOf(type, variable)) is ExpressionOutput -> copy(type = boxedTypeOf(type, returnExpressions.first())) - ArtificialBooleanOutput, EmptyOutput -> this + ArtificialBooleanOutput, is EmptyOutput -> this } } } diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractOptionsPipeline.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractOptionsPipeline.kt index 1265cf19a4a2..11a56ee77fc8 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractOptionsPipeline.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractOptionsPipeline.kt @@ -1,126 +1,239 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl +import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil +import com.intellij.codeInsight.daemon.impl.quickfix.AnonymousTargetClassPreselectionUtil +import com.intellij.codeInsight.navigation.NavigationUtil +import com.intellij.ide.util.PsiClassListCellRenderer +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.util.TextRange +import com.intellij.pom.java.LanguageLevel import com.intellij.psi.* +import com.intellij.psi.search.PsiElementProcessor import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiTypesUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.refactoring.extractMethod.PrepareFailedException +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.findUsedTypeParameters import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.hasExplicitModifier +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.inputParameterOf +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.normalizedAnchor import com.intellij.refactoring.extractMethod.newImpl.structures.ExtractOptions import com.intellij.refactoring.extractMethod.newImpl.structures.InputParameter import com.intellij.refactoring.util.VariableData -private fun findFoldableArrayExpression(reference: PsiElement): PsiArrayAccessExpression? { - val arrayAccess = reference.parent as? PsiArrayAccessExpression - return when (arrayAccess?.arrayExpression) { - reference -> arrayAccess - else -> null - } -} +object ExtractMethodPipeline { -fun remap(extractOptions: ExtractOptions, - variableData: Array, - methodName: String, - isStatic: Boolean, - visibility: String, - isConstructor: Boolean): ExtractOptions { - val analyzer = CodeFragmentAnalyzer(extractOptions.elements) - val remappedName = extractOptions.withMappedName(methodName) - val remappedInput = remappedName.withMappedParametersInput(variableData.toList()) - val remappedStatic = remappedInput.takeIf { isStatic } - ?.withForcedStatic(analyzer = analyzer) - ?: remappedInput - val mappedToConstructor = remappedStatic.takeIf { isConstructor }?.asConstructor(analyzer) ?: remappedStatic + fun remap(extractOptions: ExtractOptions, + variableData: Array, + methodName: String, + isStatic: Boolean, + visibility: String, + isConstructor: Boolean, + returnType: PsiType + ): ExtractOptions { + val analyzer = CodeFragmentAnalyzer(extractOptions.elements) + var options = withMappedName(extractOptions, methodName) + options = withMappedParametersInput(options, variableData.toList()) + val targetClass = extractOptions.anchor.containingClass!! + options = when { + targetClass.isInterface -> adjustModifiersForInterface(options.copy(visibility = PsiModifier.PRIVATE)) + else -> options.copy(visibility = visibility) + } - return mappedToConstructor.copy(visibility = visibility) -} + if (isStatic && ! options.isStatic) { + options = withForcedStatic(analyzer, options) ?: throw PrepareFailedException("Fail", options.elements.first()) + } -fun ExtractOptions.withMappedParametersInput(variablesData: List): ExtractOptions { - fun findMappedParameter(variableData: VariableData): InputParameter? { - return inputParameters - .find { (it.references.first() as? PsiReferenceExpression)?.text == variableData.variable.name } - ?.copy(name = variableData.name ?: "x", type = variableData.type) + if (isConstructor) { + options = asConstructor(analyzer, options) + } + return options.copy(dataOutput = extractOptions.dataOutput.withType(returnType)) } - val mappedParameters = variablesData.mapNotNull(::findMappedParameter) + fun withTargetClass(analyzer: CodeFragmentAnalyzer, extractOptions: ExtractOptions, targetClass: PsiClass): ExtractOptions? { + val anchor = extractOptions.anchor + if (anchor.parent == targetClass) return extractOptions - if (mappedParameters.size != inputParameters.size) return this + val newAnchor = targetClass.children.find { child -> anchor.textRange in child.textRange } as? PsiMember + if (newAnchor == null) return null - return copy( - inputParameters = mappedParameters - ) -} + val typeParameters = findAllTypeLists(anchor, targetClass).flatMap { findUsedTypeParameters(it, extractOptions.elements) } -fun ExtractOptions.withMappedName(methodName: String) = if (this.isConstructor) this else this.copy(methodName = methodName) - -fun ExtractOptions.withDefaultStatic(): ExtractOptions { - val parent = ExtractMethodHelper.getValidParentOf(elements.first()) - val parentHolder = PsiTreeUtil.getNonStrictParentOfType(parent, PsiMethod::class.java, PsiClassInitializer::class.java, - PsiField::class.java) - val shouldBeStatic = when { - parent is PsiField -> true - parentHolder.hasExplicitModifier(PsiModifier.STATIC) -> true - else -> false - } - return copy(isStatic = shouldBeStatic) -} - -fun ExtractOptions.withFoldedArrayParameters(analyzer: CodeFragmentAnalyzer): ExtractOptions { - val writtenVariables = analyzer.findWrittenVariables().mapNotNull { it.name } - - fun findFoldedCandidate(inputParameter: InputParameter): InputParameter? { - val arrayAccesses = inputParameter.references.map { findFoldableArrayExpression(it) ?: return null } - if (arrayAccesses.any { (it.parent as? PsiAssignmentExpression)?.lExpression == it }) return null - if (!ExtractMethodHelper.areSame(arrayAccesses.map { it.indexExpression })) return null - if (arrayAccesses.any { it.indexExpression?.text in writtenVariables }) return null - val parameterName = arrayAccesses.first().arrayExpression.text + "Element" - return InputParameter(arrayAccesses, parameterName, arrayAccesses.first().type ?: return null) + val additionalReferences = analyzer.findOuterLocals(anchor, newAnchor) ?: return null + val additionalParameters = additionalReferences.map { inputParameterOf(it) } + val options = extractOptions.copy( + anchor = normalizedAnchor(newAnchor), + inputParameters = extractOptions.inputParameters + additionalParameters, + typeParameters = typeParameters + ) + return withDefaultStatic(options) } - fun findHiddenExpression(arrayAccess: PsiArrayAccessExpression?): List { - return inputParameters.filter { - ExtractMethodHelper.areSame(it.references.first(), arrayAccess?.arrayExpression) - || ExtractMethodHelper.areSame(it.references.first(), arrayAccess?.indexExpression) + private fun findAllTypeLists(element: PsiElement, stopper: PsiElement): List { + return generateSequence (element) { it.parent } + .takeWhile { it != stopper && it !is PsiFile } + .filterIsInstance() + .mapNotNull { it.typeParameterList } + .toList() + } + + private fun findCommonCastParameter(inputParameter: InputParameter): InputParameter? { + val castExpressions = inputParameter.references.map { reference -> (reference.parent as? PsiTypeCastExpression) ?: return null } + val type = castExpressions.first().castType?.type ?: return null + if ( castExpressions.any { castExpression -> castExpression.castType?.type != type } ) return null + return InputParameter(name = inputParameter.name, type = type, references = castExpressions) + } + + fun withCastedParameters(extractOptions: ExtractOptions): ExtractOptions { + val parameters = extractOptions.inputParameters.map { inputParameter -> findCommonCastParameter(inputParameter) ?: inputParameter } + return extractOptions.copy(inputParameters = parameters) + } + + fun withMappedParametersInput(extractOptions: ExtractOptions, variablesData: List): ExtractOptions { + fun findMappedParameter(variableData: VariableData): InputParameter? { + return extractOptions.inputParameters + .find { parameter -> parameter.name == variableData.variable.name } + ?.copy(name = variableData.name ?: "x", type = variableData.type) + } + + val mappedParameters = variablesData.filter { it.passAsParameter }.mapNotNull(::findMappedParameter) + val disabledParameters = variablesData.filterNot { it.passAsParameter }.mapNotNull(::findMappedParameter) + + return extractOptions.copy( + inputParameters = mappedParameters, + disabledParameters = disabledParameters + ) + } + + fun adjustModifiersForInterface(options: ExtractOptions): ExtractOptions { + val targetClass = options.anchor.containingClass!! + if (! targetClass.isInterface) return options + val languageLevel: LanguageLevel = PsiUtil.getLanguageLevel(targetClass) + val isAtLeastJava9 = languageLevel.isAtLeast(LanguageLevel.JDK_1_9) + val isJava8 = languageLevel.isAtLeast(LanguageLevel.JDK_1_8) && !isAtLeastJava9 + val visibility = if (options.visibility == PsiModifier.PRIVATE && isJava8) null else options.visibility + val holder = findClassMember(options.elements.first()) + val isStatic = holder is PsiField || options.isStatic + return options.copy(visibility = visibility, isStatic = isStatic) + } + + fun withMappedName(extractOptions: ExtractOptions, methodName: String) = if (extractOptions.isConstructor) extractOptions else extractOptions.copy(methodName = methodName) + + fun withDefaultStatic(extractOptions: ExtractOptions): ExtractOptions { + val expression = extractOptions.elements.singleOrNull() as? PsiExpression + val statement = PsiTreeUtil.getParentOfType(expression, PsiExpressionStatement::class.java) + if (statement != null && JavaHighlightUtil.isSuperOrThisCall(statement, true, true)) { + return extractOptions.copy(isStatic = true) + } + val shouldBeStatic = when { + extractOptions.anchor.hasExplicitModifier(PsiModifier.STATIC) -> true + else -> false + } + return extractOptions.copy(isStatic = shouldBeStatic) + } + + fun findTargetCandidates(analyzer: CodeFragmentAnalyzer, options: ExtractOptions): List { + return generateSequence (options.anchor as PsiElement) { it.parent } + .takeWhile { it !is PsiFile } + .filterIsInstance() + .filter { targetClass -> withTargetClass(analyzer, options, targetClass) != null } + .toList() + } + + fun findDefaultTargetCandidate(candidates: List): PsiClass { + return AnonymousTargetClassPreselectionUtil.getPreselection(candidates, candidates.first()) ?: candidates.first() + } + + fun selectTargetClass(options: ExtractOptions, onSelected: (ExtractOptions) -> T): ExtractOptions { + val analyzer = CodeFragmentAnalyzer(options.elements) + val targetCandidates = findTargetCandidates(analyzer, options) + val preselection = findDefaultTargetCandidate(targetCandidates) + + val editor = FileEditorManager.getInstance(options.project).selectedTextEditor ?: return options + + val processor = PsiElementProcessor { selected -> + val mappedOptions = withTargetClass(analyzer, options, selected)!! + onSelected(mappedOptions) + true + } + + if (targetCandidates.size > 1) { + NavigationUtil.getPsiElementPopup(targetCandidates.toTypedArray(), PsiClassListCellRenderer(), "Choose Destination Class", processor, preselection) + .showInBestPositionFor(editor) + } else { + processor.execute(preselection) + } + + return options + } + + private fun findFoldableArrayExpression(reference: PsiElement): PsiArrayAccessExpression? { + val arrayAccess = reference.parent as? PsiArrayAccessExpression + return when (arrayAccess?.arrayExpression) { + reference -> arrayAccess + else -> null } } - val foldedCandidates = inputParameters.mapNotNull { findFoldedCandidate(it) } + fun withFoldedArrayParameters(analyzer: CodeFragmentAnalyzer, extractOptions: ExtractOptions): ExtractOptions { + val writtenVariables = analyzer.findWrittenVariables().mapNotNull { it.name } - val (folded, hidden) = foldedCandidates - .map { it to findHiddenExpression(it.references.first() as? PsiArrayAccessExpression) } - .filter { it.second.size > 1 }.unzip() - - return this.copy(inputParameters = this.inputParameters - hidden.flatten() + folded) -} - -fun ExtractOptions.asConstructor(analyzer: CodeFragmentAnalyzer): ExtractOptions { - return if (canBeConstructor(analyzer)) copy(isConstructor = true, methodName = "this") else this -} - -fun ExtractOptions.withForcedStatic(analyzer: CodeFragmentAnalyzer): ExtractOptions? { - val targetClass = PsiTreeUtil.getParentOfType(ExtractMethodHelper.getValidParentOf(elements.first()), PsiClass::class.java)!! - val fieldUsages = analyzer.findFieldUsages(targetClass, elements) - if (fieldUsages.any { it.isWrite }) return null - val fieldInputParameters = - fieldUsages.groupBy { it.field }.entries.map { (field, fieldUsages) -> - InputParameter( - references = fieldUsages.map { it.classMemberReference }, - name = field.name, - type = field.type - ) + fun findFoldedCandidate(inputParameter: InputParameter): InputParameter? { + val arrayAccesses = inputParameter.references.map { findFoldableArrayExpression(it) ?: return null } + if (arrayAccesses.any { (it.parent as? PsiAssignmentExpression)?.lExpression == it }) return null + if (!ExtractMethodHelper.areSame(arrayAccesses.map { it.indexExpression })) return null + if (arrayAccesses.any { it.indexExpression?.text in writtenVariables }) return null + val parameterName = arrayAccesses.first().arrayExpression.text + "Element" + return InputParameter(arrayAccesses, parameterName, arrayAccesses.first().type ?: return null) } - return copy(inputParameters = inputParameters + fieldInputParameters, isStatic = true) -} -private fun canBeConstructor(analyzer: CodeFragmentAnalyzer): Boolean { - val elements = analyzer.elements - val parent = ExtractMethodHelper.getValidParentOf(elements.first()) - val holderClass = PsiTreeUtil.getNonStrictParentOfType(parent, PsiClass::class.java) ?: return false - val method = PsiTreeUtil.getNonStrictParentOfType(parent, PsiMethod::class.java) ?: return false - val firstStatement = method.body?.statements?.firstOrNull() ?: return false - val startsOnBegin = firstStatement.textRange in TextRange(elements.first().textRange.startOffset, elements.last().textRange.endOffset) - val outStatements = method.body?.statements.orEmpty().dropWhile { it.textRange.endOffset <= elements.last().textRange.endOffset } - val hasOuterFinalFieldAssignments = analyzer - .findFieldUsages(holderClass, outStatements) - .any { it.isWrite && it.field.hasExplicitModifier("final") } - return method.isConstructor && startsOnBegin && !hasOuterFinalFieldAssignments && analyzer.findOutputVariables().isEmpty() + fun findHiddenExpression(arrayAccess: PsiArrayAccessExpression?): List { + return extractOptions.inputParameters.filter { + ExtractMethodHelper.areSame(it.references.first(), arrayAccess?.arrayExpression) + || ExtractMethodHelper.areSame(it.references.first(), arrayAccess?.indexExpression) + } + } + + val foldedCandidates = extractOptions.inputParameters.mapNotNull { findFoldedCandidate(it) } + + val (folded, hidden) = foldedCandidates + .map { it to findHiddenExpression(it.references.first() as? PsiArrayAccessExpression) } + .filter { it.second.size > 1 }.unzip() + + return extractOptions.copy(inputParameters = extractOptions.inputParameters - hidden.flatten() + folded) + } + + fun asConstructor(analyzer: CodeFragmentAnalyzer, extractOptions: ExtractOptions): ExtractOptions { + return if (canBeConstructor(analyzer)) extractOptions.copy(isConstructor = true, methodName = "this") else extractOptions + } + + fun withForcedStatic(analyzer: CodeFragmentAnalyzer, extractOptions: ExtractOptions): ExtractOptions? { + val targetClass = PsiTreeUtil.getParentOfType(ExtractMethodHelper.getValidParentOf(extractOptions.elements.first()), PsiClass::class.java)!! + val fieldUsages = analyzer.findFieldUsages(targetClass, extractOptions.elements) + if (fieldUsages.any { it.isWrite }) return null + val fieldInputParameters = + fieldUsages.groupBy { it.field }.entries.map { (field, fieldUsages) -> + InputParameter( + references = fieldUsages.map { it.classMemberReference }, + name = field.name, + type = field.type + ) + } + return extractOptions.copy(inputParameters = extractOptions.inputParameters + fieldInputParameters, isStatic = true) + } + + fun canBeConstructor(analyzer: CodeFragmentAnalyzer): Boolean { + val elements = analyzer.elements + val parent = ExtractMethodHelper.getValidParentOf(elements.first()) + val holderClass = PsiTreeUtil.getNonStrictParentOfType(parent, PsiClass::class.java) ?: return false + val method = PsiTreeUtil.getNonStrictParentOfType(parent, PsiMethod::class.java) ?: return false + val firstStatement = method.body?.statements?.firstOrNull() ?: return false + val startsOnBegin = firstStatement.textRange in TextRange(elements.first().textRange.startOffset, elements.last().textRange.endOffset) + val outStatements = method.body?.statements.orEmpty().dropWhile { it.textRange.endOffset <= elements.last().textRange.endOffset } + val hasOuterFinalFieldAssignments = analyzer + .findFieldUsages(holderClass, outStatements) + .any { it.isWrite && it.field.hasExplicitModifier("final") } + return method.isConstructor && startsOnBegin && !hasOuterFinalFieldAssignments && analyzer.findOutputVariables().isEmpty() + } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractSelector.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractSelector.kt index eb5be2f3c30a..1d2b057c8f34 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractSelector.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/ExtractSelector.kt @@ -5,8 +5,11 @@ import com.intellij.codeInsight.CodeInsightUtil import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.SelectionModel import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.extractMethod.PrepareFailedException import com.intellij.refactoring.introduceVariable.IntroduceVariableBase import com.intellij.refactoring.util.RefactoringUtil +import kotlin.math.exp class ExtractSelector { @@ -41,15 +44,20 @@ class ExtractSelector { fun suggestElementsToExtract(editor: Editor): List { val selectedElements = findSelectedElements(editor) - return alignElements(selectedElements) + val alignedElements = alignElements(selectedElements) + return when { + alignedElements.isEmpty() -> throw PrepareFailedException("Fail", selectedElements.first()) + else -> alignedElements + } } private fun alignElements(elements: List): List { val singleElement = elements.singleOrNull() val alignedElements = when { elements.size > 1 -> alignStatements(elements) + singleElement is PsiBlockStatement -> if (singleElement.codeBlock.firstBodyElement != null) listOf(singleElement) else emptyList() singleElement is PsiCodeBlock -> alignCodeBlock(singleElement) - singleElement is PsiStatement -> listOfNotNull(alignStatement(singleElement)) + //singleElement is PsiStatement -> listOfNotNull(alignStatement(singleElement)) singleElement is PsiExpression -> listOfNotNull(alignExpression(singleElement)) else -> elements } @@ -60,25 +68,39 @@ class ExtractSelector { } } - private fun alignExpression(expression: PsiExpression?): PsiExpression? { - return when (expression) { - null -> return null - is PsiReturnStatement -> alignExpression(expression.returnValue) - is PsiAssignmentExpression -> alignExpression(expression.rExpression) + private tailrec fun alignExpression(expression: PsiExpression?): PsiExpression? { + return when { + expression == null -> null + expression.type is PsiLambdaParameterType -> null + hasAssignmentInside(expression) -> null + isInsideAnnotation(expression) -> null + expression is PsiReturnStatement -> alignExpression(expression.returnValue) + expression is PsiParenthesizedExpression -> expression.takeIf { expression.expression != null } + //is PsiAssignmentExpression -> alignExpression(expression.rExpression) //expression.parent is PsiParenthesizedExpression -> alignExpression(expression.parent as PsiExpression) // PsiUtil.skipParenthesizedExprDown((PsiExpression)elements[0]); else -> expression } } + private fun hasAssignmentInside(expression: PsiExpression): Boolean { + return PsiTreeUtil.findChildOfType(expression, PsiAssignmentExpression::class.java, false) != null + } + private fun alignStatements(statements: List): List { - val filteredStatements = statements.dropWhile { it is PsiSwitchLabelStatement }.dropLastWhile { it is PsiSwitchLabelStatement } + val filteredStatements = statements + .dropWhile { it is PsiSwitchLabelStatement || it is PsiWhiteSpace } + .dropLastWhile { it is PsiSwitchLabelStatement || it is PsiWhiteSpace } return when { filteredStatements.any { it is PsiSwitchLabelStatement } -> emptyList() else -> filteredStatements } } + private fun isInsideAnnotation(expression: PsiExpression): Boolean { + return PsiTreeUtil.getParentOfType(expression, PsiAnnotation::class.java) != null + } + private fun alignCodeBlock(codeBlock: PsiCodeBlock): List { return when (codeBlock.parent) { is PsiSwitchStatement -> listOf(codeBlock.parent) diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MapFromDialog.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MapFromDialog.kt new file mode 100644 index 000000000000..2bd1c027fa83 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MapFromDialog.kt @@ -0,0 +1,147 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.refactoring.extractMethod.newImpl + +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.codeInsight.Nullability +import com.intellij.codeInsight.generation.GenerateMembersUtil +import com.intellij.psi.* +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.codeStyle.VariableKind +import com.intellij.psi.impl.source.codeStyle.JavaCodeStyleManagerImpl +import com.intellij.psi.search.LocalSearchScope +import com.intellij.refactoring.HelpID +import com.intellij.refactoring.extractMethod.ExtractMethodDialog +import com.intellij.refactoring.extractMethod.InputVariables +import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput +import com.intellij.refactoring.extractMethod.newImpl.structures.ExtractOptions +import com.intellij.util.containers.MultiMap +import java.util.* + +object MapFromDialog { + fun mapFromDialog(extractOptions: ExtractOptions, title: String, helpId: String): ExtractOptions? { + val dialog = createDialog(extractOptions, title, helpId) + val isOk = dialog.showAndGet() + if (isOk){ + return ExtractMethodPipeline.remap(extractOptions, dialog.chosenParameters, dialog.chosenMethodName, + dialog.isMakeStatic, dialog.visibility, dialog.isChainedConstructor, dialog.returnType) + } else { + return null + } + } + + private fun createDialog(extractOptions: ExtractOptions, refactoringName: String, helpId: String): ExtractMethodDialog { + val project = extractOptions.project + val returnType = extractOptions.dataOutput.type + val thrownExceptions = extractOptions.thrownExceptions.toTypedArray() + val isStatic = extractOptions.isStatic + val typeParameters = extractOptions.typeParameters + val targetClass = extractOptions.anchor.containingClass + val elements = extractOptions.elements.toTypedArray() + val nullability = extractOptions.dataOutput.nullability + val analyzer = CodeFragmentAnalyzer(extractOptions.elements) + val canBeStatic = ExtractMethodPipeline.withForcedStatic(analyzer, extractOptions) != null + val canBeChainedConstructor = ExtractMethodPipeline.canBeConstructor(analyzer) + val variables = extractOptions.inputParameters + .map { it.references.first() as? PsiReferenceExpression } + .map { it?.resolve() as? PsiVariable } + val inputVariables = InputVariables( + variables, + extractOptions.project, + LocalSearchScope(extractOptions.elements.toTypedArray()), + false, + emptySet() + ) + val typeParameterList = PsiElementFactory.getInstance(extractOptions.project).createTypeParameterList() + typeParameters.forEach { typeParameterList.add(it) } + + val methodNames = suggestInitialMethodName(extractOptions) + + return object: ExtractMethodDialog(project, targetClass, inputVariables, returnType, typeParameterList, + thrownExceptions, isStatic, canBeStatic, canBeChainedConstructor, + refactoringName, helpId, nullability, elements, {0}) { + override fun areTypesDirected() = true + + override fun suggestMethodNames(): Array { + return methodNames + } + + override fun isVoidReturn(): Boolean = false + + override fun findOccurrences(): Array { + return when (val dataOutput = extractOptions.dataOutput) { + is DataOutput.VariableOutput -> CodeInsightUtil.findReferenceExpressions(extractOptions.anchor, dataOutput.variable) + is DataOutput.ExpressionOutput -> dataOutput.returnExpressions.toTypedArray() + else -> emptyArray() + } + } + + override fun isOutputVariable(variable: PsiVariable): Boolean { + return (extractOptions.dataOutput as? DataOutput.VariableOutput)?.variable == variable + } + + override fun checkMethodConflicts(conflicts: MultiMap) { + super.checkMethodConflicts(conflicts) + val parameters = chosenParameters + val vars: MutableMap = HashMap() + for (element in elements) { + element.accept(object : JavaRecursiveElementWalkingVisitor() { + override fun visitLocalVariable(variable: PsiLocalVariable) { + super.visitLocalVariable(variable) + vars[variable.name] = variable + } + + override fun visitClass(aClass: PsiClass) {} + }) + } + for (parameter in parameters) { + val paramName = parameter.name + val variable = vars[paramName] + if (variable != null) { + //TODO bundle + conflicts.putValue(variable, "Variable with name $paramName is already defined in the selected scope") + } + } + } + + override fun hasPreviewButton() = false + } + } + + private fun suggestInitialMethodName(options: ExtractOptions): Array { + val project = options.project + val initialMethodNames: MutableSet = LinkedHashSet() + val codeStyleManager = JavaCodeStyleManager.getInstance(project) as JavaCodeStyleManagerImpl + val returnType = options.dataOutput.type + + val expression = options.elements.singleOrNull() as? PsiExpression + if (expression != null || returnType !is PsiPrimitiveType) { + codeStyleManager.suggestVariableName(VariableKind.FIELD, null, expression, returnType).names + .map { name -> codeStyleManager.variableNameToPropertyName(name, VariableKind.FIELD) } + .forEach { name -> + initialMethodNames += name + } + } + + val outVariable = (options.dataOutput as? DataOutput.VariableOutput)?.variable + if (outVariable != null) { + val outKind = codeStyleManager.getVariableKind(outVariable) + val propertyName = codeStyleManager.variableNameToPropertyName(outVariable.name!!, outKind) + val names = codeStyleManager.suggestVariableName(VariableKind.FIELD, propertyName, null, outVariable.type).names + names.forEach { name -> + initialMethodNames += codeStyleManager.variableNameToPropertyName(name, VariableKind.FIELD) + } + } + + val normalizedType = (returnType as? PsiEllipsisType)?.toArrayType() ?: returnType + val field = JavaPsiFacade.getElementFactory(project).createField("fieldNameToReplace", normalizedType) + fun suggestGetterName(name: String): String? { + field.name = name + return GenerateMembersUtil.suggestGetterName(field) + } + + val getters: List = initialMethodNames.filter { PsiNameHelper.getInstance(project).isIdentifier(it) } + .mapNotNull { propertyName -> suggestGetterName(propertyName) } + return getters.toTypedArray() + } + +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MethodExtractor.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MethodExtractor.kt index 2b0d483d49d5..e470968b4242 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MethodExtractor.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/MethodExtractor.kt @@ -3,84 +3,218 @@ package com.intellij.refactoring.extractMethod.newImpl import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.* import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.impl.source.PostprocessReformattingAspect +import com.intellij.refactoring.HelpID import com.intellij.refactoring.extractMethod.ExtractMethodDialog +import com.intellij.refactoring.extractMethod.ExtractMethodHandler +import com.intellij.refactoring.extractMethod.PrepareFailedException import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.addSiblingAfter import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodHelper.wrapWithCodeBlock -import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput.* +import com.intellij.refactoring.extractMethod.newImpl.ExtractMethodPipeline.selectTargetClass +import com.intellij.refactoring.extractMethod.newImpl.MapFromDialog.mapFromDialog +import com.intellij.refactoring.extractMethod.newImpl.structures.DataOutput.ExpressionOutput import com.intellij.refactoring.extractMethod.newImpl.structures.ExtractOptions -import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput.* +import com.intellij.refactoring.extractMethod.newImpl.structures.FlowOutput.ConditionalFlow import com.intellij.refactoring.introduceVariable.IntroduceVariableBase +import com.intellij.refactoring.listeners.RefactoringEventData +import com.intellij.refactoring.listeners.RefactoringEventListener +import com.intellij.refactoring.util.CommonRefactoringUtil +import com.intellij.util.IncorrectOperationException -fun extractMethod(dependencies: ExtractOptions) { - val factory = PsiElementFactory.getInstance(dependencies.project) - val styleManager = CodeStyleManager.getInstance(dependencies.project) - val flowOutput = dependencies.flowOutput - val newFlowOutput = if (dependencies.dataOutput is ExpressionOutput && flowOutput is ConditionalFlow) { - flowOutput.copy(statements = flowOutput.statements.filterNot { it is PsiReturnStatement }) - } - else { - flowOutput - } - val codeBlock = with(dependencies) { - BodyBuilder(factory).build( - elements = elements, - flowOutput = newFlowOutput, - dataOutput = dataOutput, - inputParameters = inputParameters, - missedDeclarations = requiredVariablesInside - ) - } - val signature = SignatureBuilder(dependencies.project) - .build( - isStatic = dependencies.isStatic, - visibility = dependencies.visibility, - typeParameters = dependencies.typeParameters, - returnType = dependencies.dataOutput.type.takeIf { !dependencies.isConstructor }, - methodName = dependencies.methodName, - inputParameters = dependencies.inputParameters, - thrownExceptions = dependencies.thrownExceptions - ) - val method = styleManager.reformat(signature) as PsiMethod - method.body?.replace(codeBlock) +class MethodExtractor { - if (needsNullabilityAnnotations(dependencies.project)) { - updateMethodAnnotations(method) - } + private val LOG = Logger.getInstance(MethodExtractor::class.java) - ApplicationManager.getApplication().runWriteAction { - - dependencies.anchor.addSiblingAfter(method) - - val methodCall = dependencies.methodName + "(" + dependencies.inputParameters.joinToString { it.references.first().text } + ")" - val expressionElement = (dependencies.elements.singleOrNull() as? PsiExpression) - if (expressionElement != null) { - val callExpression = PsiElementFactory.getInstance(expressionElement.project).createExpressionFromText(methodCall, null) - IntroduceVariableBase.replace(expressionElement, callExpression, expressionElement.project) + fun doExtract(editor: Editor, refactoringName: String, helpId: String): Boolean { + val statements = ExtractSelector().suggestElementsToExtract(editor) + try { + val extractOptions = findExtractOptions(statements) ?: return false + selectTargetClass(extractOptions) { targetOptions -> + val withDialog = mapFromDialog(targetOptions, refactoringName, helpId) ?: return@selectTargetClass false + val project = withDialog.project + CommandProcessor.getInstance().executeCommand( + project, + { PostprocessReformattingAspect.getInstance(project).postponeFormattingInside { doRefactoring(withDialog) } }, + ExtractMethodHandler.getRefactoringName(), null) + return@selectTargetClass false + } + } catch (e: PrepareFailedException){ + CommonRefactoringUtil.showErrorHint(statements.first().project, editor, e.message!!, refactoringName, HelpID.EXTRACT_METHOD) } - else { - val callElements = CallFactory(PsiElementFactory.getInstance(dependencies.project)).buildCall( - methodCall = methodCall, - flowOutput = dependencies.flowOutput, - dataOutput = dependencies.dataOutput, - exposedDeclarations = dependencies.exposedLocalVariables + + return true + } + + private fun doRefactoring(options: ExtractOptions){ + try { + val beforeData = RefactoringEventData() + beforeData.addElements(options.elements.toTypedArray()) + options.project.messageBus.syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC) + .refactoringStarted("refactoring.extract.method", beforeData) + val method = extractMethod(options) + val data = RefactoringEventData() + data.addElement(method) + options.project.messageBus.syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC) + .refactoringDone("refactoring.extract.method", data) + } + catch (e: IncorrectOperationException) { + LOG.error(e) + } + } + + fun doTestExtract( + doRefactor: Boolean, + editor: Editor, + isConstructor: Boolean?, + isStatic: Boolean?, + returnType: PsiType?, + newNameOfFirstParam: String?, + targetClass: PsiClass?, + @PsiModifier.ModifierConstant visibility: String?, + vararg disabledParameters: Int + ): Boolean { + val elements = ExtractSelector().suggestElementsToExtract(editor) + var options = findExtractOptions(elements) ?: throw PrepareFailedException("Fail", elements.first()) + val analyzer = CodeFragmentAnalyzer(elements) + + val candidates = ExtractMethodPipeline.findTargetCandidates(analyzer, options) + options = ExtractMethodPipeline.withTargetClass(analyzer, options, candidates.firstOrNull { it !is PsiAnonymousClass } ?: candidates.first() ) + ?: throw PrepareFailedException("Fail", elements.first()) + options = options.copy(methodName = "newMethod") + if (isConstructor != options.isConstructor){ + options = ExtractMethodPipeline.asConstructor(analyzer, options) + } + if (! options.isStatic && isStatic == true) { + options = ExtractMethodPipeline.withForcedStatic(analyzer, options) ?: throw PrepareFailedException("Fail", elements.first()) + } + if (newNameOfFirstParam != null) { + options = options.copy( + inputParameters = listOf(options.inputParameters.first().copy(name = newNameOfFirstParam)) + options.inputParameters.drop(1) ) - - val elementsToAdd = when { - callElements.size > 1 && dependencies.elements.first().parent !is PsiCodeBlock -> wrapWithCodeBlock(callElements) - else -> callElements - } - elementsToAdd.reversed().forEach { statement -> - val addedStatement = dependencies.elements.last().addSiblingAfter(statement) - styleManager.reformat(addedStatement) - } - dependencies.elements.first().parent.deleteChildRange(dependencies.elements.first(), dependencies.elements.last()) } + if (returnType != null) { + options = options.copy(dataOutput = options.dataOutput.withType(returnType)) + } + if (targetClass != null) { + options = ExtractMethodPipeline.withTargetClass(analyzer, options, targetClass) ?: throw PrepareFailedException("Fail", elements.first()) + } + if (disabledParameters.isNotEmpty()) { + options = options.copy( + disabledParameters = options.inputParameters.filterIndexed { index, _ -> index in disabledParameters }, + inputParameters = options.inputParameters.filterIndexed { index, _ -> index !in disabledParameters } + ) + } + if (visibility != null) { + options = options.copy(visibility = visibility) + } + if (options.anchor.containingClass?.isInterface == true) { + options = ExtractMethodPipeline.adjustModifiersForInterface(options.copy(visibility = PsiModifier.PRIVATE)) + } + if (doRefactor) { + extractMethod(options) + } + return true + } + + fun extractMethod(dependencies: ExtractOptions): PsiMethod { + val factory = PsiElementFactory.getInstance(dependencies.project) + val styleManager = CodeStyleManager.getInstance(dependencies.project) + val flowOutput = dependencies.flowOutput + val newFlowOutput = when { + dependencies.dataOutput is ExpressionOutput && flowOutput is ConditionalFlow -> + flowOutput.copy(statements = flowOutput.statements.filterNot { it is PsiReturnStatement }) + else -> flowOutput + } + val codeBlock = with(dependencies) { + BodyBuilder(factory).build( + elements = elements, + flowOutput = newFlowOutput, + dataOutput = dataOutput, + inputParameters = inputParameters, + missedDeclarations = requiredVariablesInside, + disabledParameters = disabledParameters + ) + } + val method = SignatureBuilder(dependencies.project) + .build( + context = dependencies.anchor.context, + scope = dependencies.elements, + isStatic = dependencies.isStatic, + visibility = dependencies.visibility, + typeParameters = dependencies.typeParameters, + returnType = dependencies.dataOutput.type.takeIf { !dependencies.isConstructor }, + methodName = dependencies.methodName, + inputParameters = dependencies.inputParameters, + thrownExceptions = dependencies.thrownExceptions, + anchor = dependencies.anchor + ) + method.body?.replace(codeBlock) + + val methodCall = findExtractQualifier(dependencies) + "(" + dependencies.inputParameters.map { it.references.first() }.joinToString { it.text } + ")" + + val callBuilder = CallBuilder(dependencies.project, dependencies.elements.first().context) + val expressionElement = (dependencies.elements.singleOrNull() as? PsiExpression) + val callElements = when (expressionElement) { + null -> callBuilder.buildCall(methodCall, dependencies.flowOutput, dependencies.dataOutput, dependencies.exposedLocalVariables) + else -> callBuilder.buildExpressionCall(methodCall, dependencies.dataOutput) + } + val formattedCallElements = callElements.map { styleManager.reformat(it) } + + if (needsNullabilityAnnotations(dependencies.project)) { + updateMethodAnnotations(method, dependencies.inputParameters) + } + + var addedMethod: PsiMethod? = null + ApplicationManager.getApplication().runWriteAction { + addedMethod = dependencies.anchor.addSiblingAfter(method) as PsiMethod + replace(dependencies.elements, formattedCallElements) + } + + return addedMethod!! + } + + private fun replace(source: List, target: List) { + val sourceAsExpression = source.singleOrNull() as? PsiExpression + val targetAsExpression = target.singleOrNull() as? PsiExpression + if (sourceAsExpression != null && targetAsExpression != null) { + IntroduceVariableBase.replace(sourceAsExpression, targetAsExpression, sourceAsExpression.project) + return + } + + val normalizedTarget = when { + target.size > 1 && source.first().parent !is PsiCodeBlock -> wrapWithCodeBlock(target) + else -> target + } + normalizedTarget.reversed().forEach { statement -> + source.last().addSiblingAfter(statement) + } + source.first().parent.deleteChildRange(source.first(), source.last()) + } + + private fun needsNullabilityAnnotations(project: Project): Boolean { + return PropertiesComponent.getInstance(project).getBoolean(ExtractMethodDialog.EXTRACT_METHOD_GENERATE_ANNOTATIONS, true) } } -private fun needsNullabilityAnnotations(project: Project): Boolean { - return PropertiesComponent.getInstance(project).getBoolean(ExtractMethodDialog.EXTRACT_METHOD_GENERATE_ANNOTATIONS, true) +private fun findExtractQualifier(options: ExtractOptions): String { + val callText = options.methodName + "(" + options.inputParameters.map { it.references.first() }.joinToString { it.text } + ")" + val factory = PsiElementFactory.getInstance(options.project) + val callElement = factory.createExpressionFromText(callText, options.elements.first().context) as PsiMethodCallExpression + val targetClassName = options.anchor.containingClass?.name + val member = findClassMember(options.elements.first()) + if (member == options.anchor) return options.methodName + if (callElement.resolveMethod() != null && !options.isConstructor) { + return when { + options.isStatic -> "$targetClassName.${options.methodName}" + else -> "$targetClassName.this.${options.methodName}" + } + } else { + return options.methodName + } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/SignatureBuilder.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/SignatureBuilder.kt index 6dec45763579..714de76e3728 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/SignatureBuilder.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/SignatureBuilder.kt @@ -2,35 +2,85 @@ package com.intellij.refactoring.extractMethod.newImpl import com.intellij.openapi.project.Project +import com.intellij.pom.java.LanguageLevel import com.intellij.psi.* +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleSettings +import com.intellij.psi.util.PsiUtil import com.intellij.refactoring.extractMethod.newImpl.structures.InputParameter +import com.intellij.refactoring.util.RefactoringUtil -class SignatureBuilder(project: Project) { +class SignatureBuilder(private val project: Project) { private val factory: PsiElementFactory = PsiElementFactory.getInstance(project) fun build( + context: PsiElement?, + scope: List, isStatic: Boolean = false, - visibility: String, - typeParameters: PsiTypeParameterList, + visibility: String?, + typeParameters: List, returnType: PsiType? = null, methodName: String = "extracted", inputParameters: List = emptyList(), - thrownExceptions: List = emptyList() + thrownExceptions: List = emptyList(), + anchor: PsiMember ): PsiMethod { - val parameterList = factory.createParameterList( - inputParameters.map { it.name }.toTypedArray(), - inputParameters.map { it.type }.toTypedArray() - ) + + val parameterList = createParameterList(inputParameters, scope) + val method = when (returnType) { - null -> factory.createConstructor() - else -> factory.createMethod(methodName, returnType) + null -> factory.createConstructor("methodName", context) + else -> factory.createMethod(methodName, returnType, context) } - method.typeParameterList?.replace(typeParameters) + + JavaCodeStyleManager.getInstance(method.project).shortenClassReferences(method) + + val isInInterface = anchor.containingClass?.isInterface == true + val shouldHaveDefaultModifier = isJava8() && ! isStatic && isInInterface + + val typeParameterList = factory.createTypeParameterList() + typeParameters.forEach { typeParameterList.add(it) } + method.typeParameterList?.replace(typeParameterList) method.parameterList.replace(parameterList) method.modifierList.setModifierProperty(PsiModifier.STATIC, isStatic) - method.modifierList.setModifierProperty(visibility, true) + method.modifierList.setModifierProperty(PsiModifier.DEFAULT, shouldHaveDefaultModifier) + if (visibility != null) method.modifierList.setModifierProperty(visibility, true) thrownExceptions.map { exception -> factory.createReferenceElementByType(exception) }.forEach { method.throwsList.add(it) } return method } + private fun createParameterList(inputParameters: List, scope: List): PsiParameterList { + val parameterList = factory.createParameterList( + inputParameters.map { it.name }.toTypedArray(), + inputParameters.map { it.type }.toTypedArray() + ) + + if (inputParameters.isEmpty()) return parameterList + + val element = inputParameters.first().references.first() + val useDefaultFinal = JavaCodeStyleSettings.getInstance(scope.first().project).GENERATE_FINAL_PARAMETERS + + inputParameters.forEach { parameter -> + val shouldBeFinal = when { + useDefaultFinal -> parameter.references.none { reference -> PsiUtil.isAccessedForWriting(reference) } + ! PsiUtil.isLanguageLevel8OrHigher(element) -> parameter.references.any { reference -> isInsideAnonymousOrLocal(reference, scope) } + else -> false + } + val methodParameter = parameterList.parameters.find { it.name == parameter.name } + PsiUtil.setModifierProperty(methodParameter!!, PsiModifier.FINAL, shouldBeFinal) + } + + return parameterList + } + + private fun isInsideAnonymousOrLocal(element: PsiElement, scope: List): Boolean { + return scope.any { upperBound -> RefactoringUtil.isInsideAnonymousOrLocal(element, upperBound) } + } + + private fun isJava8(): Boolean { + val languageLevel: LanguageLevel = PsiUtil.getLanguageLevel(project) + val isAtLeastJava9 = languageLevel.isAtLeast(LanguageLevel.JDK_1_9) + return languageLevel.isAtLeast(LanguageLevel.JDK_1_8) && !isAtLeastJava9 + } + } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/CodeFragment.java b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/CodeFragment.java deleted file mode 100644 index d04f0f198a17..000000000000 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/CodeFragment.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.refactoring.extractMethod.newImpl.structures; - -import com.intellij.codeInsight.CodeInsightUtil; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.*; -import com.intellij.psi.util.PsiTreeUtil; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class CodeFragment { - public final List elements; - - private CodeFragment(List elements) { - this.elements = elements; - } - - public static CodeFragment of(List elements) { - checkRange(elements); - return new CodeFragment(elements); - } - - public PsiElement getFirstElement() { - return elements.get(0); - } - - public PsiElement getLastElement() { - return elements.get(elements.size() - 1); - } - - public PsiElement getCommonParent() { - return getFirstElement().getParent(); - } - - public Project getProject() { - return getFirstElement().getProject(); - } - - public PsiFile getContainingFile() { - return getFirstElement().getContainingFile(); - } - - public TextRange getTextRange() { - return new TextRange(getFirstElement().getTextRange().getStartOffset(), getLastElement().getTextRange().getEndOffset()); - } - - private static void checkRange(List elements) { - if (elements.isEmpty()) throw new IllegalArgumentException(); - final PsiElement parent = elements.get(0).getParent(); - final boolean areNotSiblings = elements.stream().anyMatch(element -> element.getParent() != parent); - if (parent == null || areNotSiblings) throw new IllegalArgumentException(); - } - - public static CodeFragment copyOf(CodeFragment codeFragment) { - final PsiCodeBlock block = PsiElementFactory.getInstance(codeFragment.getProject()).createCodeBlockFromText("{}", codeFragment.getFirstElement().getContext()); - block.addRange(codeFragment.getFirstElement(), codeFragment.getLastElement()); - final List elements = PsiTreeUtil.getElementsOfRange(block.getFirstBodyElement(), block.getLastBodyElement()); - return of(elements); - } - - @SuppressWarnings("unchecked") - @NotNull - public static T findSameElementInCopy(CodeFragment source, CodeFragment copy, T element) { - final int sourceStartOffset = source.getTextRange().getStartOffset(); - final int copyStartOffset = copy.getTextRange().getStartOffset(); - final TextRange range = element.getTextRange().shiftRight(copyStartOffset - sourceStartOffset); - final PsiElement elementCopy = - CodeInsightUtil.findElementInRange(copy.getContainingFile(), range.getStartOffset(), range.getEndOffset(), element.getClass()); - return (T) elementCopy; - } -} diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/DataOutput.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/DataOutput.kt index a24a9654df41..258fca8fe899 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/DataOutput.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/DataOutput.kt @@ -1,24 +1,38 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.refactoring.extractMethod.newImpl.structures +import com.intellij.codeInsight.Nullability import com.intellij.psi.PsiExpression import com.intellij.psi.PsiType import com.intellij.psi.PsiVariable sealed class DataOutput { abstract val type: PsiType + abstract val nullability: Nullability - data class VariableOutput(override val type: PsiType, val variable: PsiVariable, val declareType: Boolean) : DataOutput() { + data class VariableOutput(override val type: PsiType, val variable: PsiVariable, val declareType: Boolean, + override val nullability: Nullability = Nullability.UNKNOWN) : DataOutput() { val name: String = requireNotNull(variable.name) } - data class ExpressionOutput(override val type: PsiType, val name: String, val returnExpressions: List) : DataOutput() + data class ExpressionOutput(override val type: PsiType, val name: String?, val returnExpressions: List, + override val nullability: Nullability = Nullability.UNKNOWN) : DataOutput() object ArtificialBooleanOutput : DataOutput() { override val type: PsiType = PsiType.BOOLEAN + override val nullability: Nullability = Nullability.UNKNOWN } - object EmptyOutput : DataOutput() { - override val type: PsiType = PsiType.VOID + data class EmptyOutput(override val type: PsiType = PsiType.VOID) : DataOutput(){ + override val nullability: Nullability = Nullability.UNKNOWN + } + + fun withType(type: PsiType): DataOutput { + return when(this) { + is VariableOutput -> this.copy(type = type) + is ExpressionOutput -> this.copy(type = type) + is EmptyOutput -> this.copy(type = type) + ArtificialBooleanOutput -> this + } } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/ExtractOptions.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/ExtractOptions.kt index 51ac453cc4e9..a0c71c0fbf18 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/ExtractOptions.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/ExtractOptions.kt @@ -2,24 +2,22 @@ package com.intellij.refactoring.extractMethod.newImpl.structures import com.intellij.openapi.project.Project -import com.intellij.psi.PsiClassType -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiTypeParameterList -import com.intellij.psi.PsiVariable +import com.intellij.psi.* data class ExtractOptions( - val anchor: PsiElement, + val anchor: PsiMember, val elements: List, val flowOutput: FlowOutput, val dataOutput: DataOutput, val thrownExceptions: List, val requiredVariablesInside: List, val inputParameters: List, - val typeParameters: PsiTypeParameterList, + val typeParameters: List, val methodName: String, val isStatic: Boolean, - val visibility: String, + val visibility: String?, val exposedLocalVariables: List, + val disabledParameters: List, val isConstructor: Boolean ) { val project: Project diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/InputParameter.kt b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/InputParameter.kt index 43e3cbdde447..d3b1834d4cf6 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/InputParameter.kt +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/newImpl/structures/InputParameter.kt @@ -4,4 +4,8 @@ package com.intellij.refactoring.extractMethod.newImpl.structures import com.intellij.psi.PsiExpression import com.intellij.psi.PsiType -data class InputParameter(val references: List, val name: String, val type: PsiType) \ No newline at end of file +data class InputParameter(val references: List, val name: String, val type: PsiType) { + init { + require(references.isNotEmpty()) + } +} \ No newline at end of file