diff --git a/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml b/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml index 244e7fb5bbb5..2abed09efa7c 100644 --- a/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml +++ b/python/python-psi-impl/resources/META-INF/PythonPsiImpl.xml @@ -358,6 +358,11 @@ com.jetbrains.python.codeInsight.intentions.PyAnnotateVariableTypeIntention Python + + + com.jetbrains.python.codeInsight.intentions.PyInvertIfConditionIntention + Python + diff --git a/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/after.py.template b/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/after.py.template new file mode 100644 index 000000000000..40b1f3457ff4 --- /dev/null +++ b/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/after.py.template @@ -0,0 +1,4 @@ +if a != b: + return b +else: + return a \ No newline at end of file diff --git a/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/before.py.template b/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/before.py.template new file mode 100644 index 000000000000..6a0936dc0b99 --- /dev/null +++ b/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/before.py.template @@ -0,0 +1,4 @@ +if a == b: + return a +else: + return b \ No newline at end of file diff --git a/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/description.html b/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/description.html new file mode 100644 index 000000000000..eaa38b75c4e0 --- /dev/null +++ b/python/python-psi-impl/resources/intentionDescriptions/PyInvertIfConditionIntention/description.html @@ -0,0 +1,7 @@ + + + + This intention inverts if condition branches. + + + \ No newline at end of file diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 79027de8331c..7d7f264d75a5 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -267,6 +267,9 @@ INTN.convert.method.to.property=Convert method to property INTN.convert.relative.to.absolute=Convert relative import to absolute INTN.convert.absolute.to.relative=Convert absolute import to relative +#PyInvertIfConditionIntention +INTN.invert.if.condition=Invert 'if' condition + ### Quick fixes ### QFIX.add.qualifier=Add qualifier diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/ConditionUtil.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/ConditionUtil.kt new file mode 100644 index 000000000000..01e864107905 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/ConditionUtil.kt @@ -0,0 +1,248 @@ +// 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.jetbrains.python.codeInsight + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.collectDescendantsOfType +import com.intellij.psi.util.parents +import com.intellij.util.IncorrectOperationException +import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.impl.PyPsiUtils + +/** + * Conditional expressions utility + * + * @author Vasya Aksyonov, Alexey.Ivanov + */ +object ConditionUtil { + private val comparisonStrings = hashMapOf( + PyTokenTypes.LT to "<", + PyTokenTypes.GT to ">", + PyTokenTypes.EQEQ to "==", + PyTokenTypes.LE to "<=", + PyTokenTypes.GE to ">=", + PyTokenTypes.NE to "!=", + PyTokenTypes.NE_OLD to "<>" + ) + + private val invertedComparisons = hashMapOf( + PyTokenTypes.LT to PyTokenTypes.GE, + PyTokenTypes.GT to PyTokenTypes.LE, + PyTokenTypes.EQEQ to PyTokenTypes.NE, + PyTokenTypes.LE to PyTokenTypes.GT, + PyTokenTypes.GE to PyTokenTypes.LT, + PyTokenTypes.NE to PyTokenTypes.EQEQ, + PyTokenTypes.NE_OLD to PyTokenTypes.EQEQ + ) + + @JvmStatic + fun findComparisonNegationOperators(expression: PyBinaryExpression?): Pair? { + val comparisonExpression = findComparisonExpression(expression) ?: return null + return comparisonStrings.getValue(comparisonExpression.operator) to + comparisonStrings.getValue(invertedComparisons.getValue(comparisonExpression.operator)) + } + + @JvmStatic + fun findComparisonExpression(expression: PyBinaryExpression?): PyBinaryExpression? { + var comparisonExpression = expression + while (comparisonExpression != null) { + if (comparisonStrings.containsKey(comparisonExpression.operator)) { + return comparisonExpression + } + comparisonExpression = PsiTreeUtil.getParentOfType(expression, PyBinaryExpression::class.java) + } + return null + } + + @JvmStatic + fun negateComparisonExpression(project: Project, file: PsiFile, expression: PyBinaryExpression?): PsiElement? { + val comparisonExpression = findComparisonExpression(expression) ?: return null + + val level = LanguageLevel.forElement(file) + val elementGenerator = PyElementGenerator.getInstance(project) + + val parent = findNonParenthesizedExpressionParent(comparisonExpression) + val invertedOperator = invertedComparisons.getValue(comparisonExpression.operator) + val invertedExpression = elementGenerator.createBinaryExpression( + comparisonStrings.getValue(invertedOperator), + comparisonExpression.leftExpression, + comparisonExpression.rightExpression) + + if (parent is PyPrefixExpression && parent.operator === PyTokenTypes.NOT_KEYWORD) { + return parent.replace(invertedExpression) + } + else { + return comparisonExpression.replace(elementGenerator.createExpressionFromText(level, "not " + invertedExpression.text)) + } + } + + @JvmStatic + fun invertConditionalExpression(project: Project, file: PsiFile, expression: PyExpression): PyExpression { + val level = LanguageLevel.forElement(file) + val elementGenerator = PyElementGenerator.getInstance(project) + val invertedExpression = getInvertedConditionExpression(project, file, level, elementGenerator, expression, true) + return expression.replace(invertedExpression) as PyExpression + } + + private fun getInvertedConditionExpression( + project: Project, + file: PsiFile, + level: LanguageLevel, + generator: PyElementGenerator, + expression: PyExpression, + isTopLevelExpression: Boolean): PyExpression { + if (expression is PyParenthesizedExpression) { + return getInvertedConditionExpression( + project, file, level, generator, expression.containedExpression!!, isTopLevelExpression) + } + + if (expression is PyPrefixExpression && expression.operator == PyTokenTypes.NOT_KEYWORD) { + val invertedExpression = expression.operand!! + return if (isTopLevelExpression && !requiresParentheses(invertedExpression)) + PyPsiUtils.flattenParens(invertedExpression)!! + else + invertedExpression + } + + if (expression !is PyBinaryExpression) { + val expressionBuilder = StringBuilder("not ") + if (expression is PyAssignmentExpression || requiresParentheses(expression)) { + expressionBuilder.append("(") + expressionBuilder.append(expression.text) + expressionBuilder.append(")") + } + else { + expressionBuilder.append(expression.text) + } + return generator.createExpressionFromText(level, expressionBuilder.toString()) + } + + if (expression.operator == PyTokenTypes.IS_KEYWORD) { + val isNegative = expression.node.findChildByType(PyTokenTypes.NOT_KEYWORD) != null + return generator.createBinaryExpression( + if (isNegative) "is" else "is not", + expression.leftExpression, + expression.rightExpression) + } + + if (expression.operator == PyTokenTypes.IN_KEYWORD) { + return generator.createBinaryExpression( + "not in", + expression.leftExpression, + expression.rightExpression) + } + + if (expression.operator == PyTokenTypes.NOT_KEYWORD) { + if (expression.node.findChildByType(PyTokenTypes.IN_KEYWORD) == null) { + throw IncorrectOperationException("Unexpected NOT binary expression") + } + return generator.createBinaryExpression( + "in", + expression.leftExpression, + expression.rightExpression) + } + + if (comparisonStrings.containsKey(expression.operator)) { + val invertedOperator = invertedComparisons.getValue(expression.operator) + return generator.createBinaryExpression( + comparisonStrings.getValue(invertedOperator), + expression.leftExpression, + expression.rightExpression) + } + + if (expression.operator == PyTokenTypes.OR_KEYWORD) { + return generator.createBinaryExpression( + "and", + getInvertedConditionExpression( + project, file, level, generator, expression.leftExpression, false), + getInvertedConditionExpression( + project, file, level, generator, expression.rightExpression!!, + false)) + } + + if (expression.operator == PyTokenTypes.AND_KEYWORD) { + val adjacentConjunctions = mutableListOf() + var adjacentConjunction = expression.leftExpression + while (adjacentConjunction is PyBinaryExpression && adjacentConjunction.operator == PyTokenTypes.AND_KEYWORD) { + adjacentConjunctions.add(adjacentConjunction.leftExpression) + adjacentConjunctions.add(adjacentConjunction.rightExpression!!) + adjacentConjunction = adjacentConjunction.leftExpression + } + + val expressionBuilder = StringBuilder() + + if (!isTopLevelExpression) { + expressionBuilder.append("(") + } + + if (adjacentConjunctions.isEmpty()) { + val invertedExpression = getInvertedConditionExpression( + project, file, level, generator, expression.leftExpression, false) + expressionBuilder.append(invertedExpression.text) + expressionBuilder.append(" or ") + } + else { + adjacentConjunctions.forEach { + val invertedExpression = getInvertedConditionExpression( + project, file, level, generator, it, false) + expressionBuilder.append(invertedExpression.text) + expressionBuilder.append(" or ") + } + } + + val rightExpression = getInvertedConditionExpression( + project, file, level, generator, expression.rightExpression!!, false) + expressionBuilder.append(rightExpression.text) + + if (!isTopLevelExpression) { + expressionBuilder.append(")") + } + + return generator.createExpressionFromText(level, expressionBuilder.toString()) + } + + throw IncorrectOperationException("Is not a condition") + } + + private fun findNonParenthesizedExpressionParent(element: PsiElement): PsiElement { + var parent = element.parent + while (parent is PyParenthesizedExpression) { + parent = parent.getParent() + } + return parent + } + + private fun requiresParentheses(element: PsiElement): Boolean { + val allWhitespacesAreParenthesized = element.collectDescendantsOfType().map { whitespace -> + whitespace.parents.takeWhile { it != element }.firstOrNull { + it is PyParenthesizedExpression || it is PyArgumentList + } + }.all { it != null } + if (allWhitespacesAreParenthesized) { + return false + } + + var result = true + var hasTrailingBackslash = false + for (c in element.text) { + when { + StringUtil.isLineBreak(c) -> { + result = result && hasTrailingBackslash + hasTrailingBackslash = false + } + c == '\\' -> { + hasTrailingBackslash = true + } + !StringUtil.isWhiteSpace(c) -> { + hasTrailingBackslash = false + } + } + } + return !result + } +} \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt new file mode 100644 index 000000000000..b19fda2d9c53 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyInvertIfConditionIntention.kt @@ -0,0 +1,336 @@ +// 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.jetbrains.python.codeInsight.intentions + +import com.intellij.codeInsight.controlflow.ControlFlowUtil +import com.intellij.codeInspection.SuppressionUtil +import com.intellij.lang.ASTNode +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.impl.source.codeStyle.CodeEditUtil +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.parents +import com.intellij.psi.util.parentsOfType +import com.intellij.psi.util.siblings +import com.intellij.util.IncorrectOperationException +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.codeInsight.ConditionUtil +import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache +import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.impl.PyPsiUtils + +/** + * Inverts 'if' condition branches + * + * @author Vasya Aksyonov + */ +class PyInvertIfConditionIntention : PyBaseIntentionAction() { + private companion object { + val insignificantTokenSet = TokenSet.create(TokenType.WHITE_SPACE, PyTokenTypes.END_OF_LINE_COMMENT) + const val PYLINT_COMMENT_PREFIX = "# pylint:" + } + + init { + text = PyPsiBundle.message("INTN.invert.if.condition") + } + + override fun getFamilyName(): String = text + + override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean { + if (file !is PyFile) { + return false + } + + val element = file.findElementAt(editor.caretModel.offset) ?: return false + + val conditionalExpression = element.parentsOfType().firstOrNull() + if (conditionalExpression != null) { + return conditionalExpression.condition != null && + conditionalExpression.falsePart != null + } + + val ifStatement = element.parentsOfType().firstOrNull() + if (ifStatement != null) { + return ifStatement.ifPart.condition != null && + ifStatement.elifParts.isEmpty() && + isAvailableForIfStatement(element, ifStatement) + } + + return false + } + + private fun isAvailableForIfStatement(element: PsiElement, statement: PyIfStatement): Boolean { + val parents = element.parents.takeWhile { it != statement } + + if (parents.contains(statement.ifPart.statementList)) { + return false + } + + val elsePart = statement.elsePart + if (elsePart != null && parents.contains(elsePart.statementList)) { + return false + } + + return true + } + + override fun doInvoke(project: Project, editor: Editor, file: PsiFile) { + val element = file.findElementAt(editor.caretModel.offset) ?: return + + val conditionalExpression = element.parentsOfType().firstOrNull() + if (conditionalExpression != null) { + invertConditional(project, file, conditionalExpression) + return + } + + val ifStatement = element.parentsOfType().firstOrNull() + if (ifStatement != null) { + if (ifStatement.ifPart.condition == null || ifStatement.elifParts.isNotEmpty()) { + return + } + + val elsePart = ifStatement.elsePart + if (elsePart != null) { + invertIfStatementComplete(project, file, ifStatement) + return + } + + val terminableStatement = ifStatement.findTerminableParent() + if (terminableStatement == null) { + invertIfStatementIncomplete(project, editor, file, ifStatement) + return + } + + if (!ifStatement.ifPart.statementList.isTerminated && + ifStatement.parent.lastSignificantChild != ifStatement) { + invertIfStatementIncomplete(project, editor, file, ifStatement) + return + } + + invertIfStatementFollowup(project, file, ifStatement, terminableStatement) + return + } + + throw IncorrectOperationException("Is not a condition") + } + + private fun invertConditional(project: Project, file: PsiFile, expression: PyConditionalExpression) { + val condition = expression.condition + val falsePart = expression.falsePart + if (condition != null && falsePart != null) { + ConditionUtil.invertConditionalExpression(project, file, condition) + val originalFalsePart = falsePart.copy() + falsePart.replace(expression.truePart) + expression.truePart.replace(originalFalsePart) + } + } + + private fun invertIfStatementComplete(project: Project, file: PsiFile, statement: PyIfStatement) { + ConditionUtil.invertConditionalExpression(project, file, statement.ifPart.condition!!) + + val ifStatements = statement.ifPart.statementList + val elseStatements = statement.elsePart!!.statementList + + val ifLastChild = ifStatements.lastChild + val elseLastChild = elseStatements.lastChild + + elseStatements.addRange(ifStatements.firstChild, ifLastChild) + ifStatements.addRange(elseStatements.firstChild, elseLastChild) + + ifStatements.deleteChildRange(ifStatements.firstChild, ifLastChild) + elseStatements.deleteChildRange(elseStatements.firstChild, elseLastChild) + + switchAttachedComments(statement, statement.elsePart!!) + switchInlineComments(statement) + } + + private fun invertIfStatementIncomplete(project: Project, editor: Editor, file: PsiFile, statement: PyIfStatement) { + val invertedCondition = ConditionUtil.invertConditionalExpression(project, file, statement.ifPart.condition!!) + + val level = LanguageLevel.forElement(file) + val generator = PyElementGenerator.getInstance(project) + + // Switching statements + val completeStatement = generator.createFromText(level, PyIfStatement::class.java, "if a:\n\tpass\nelse:\n\tpass") + completeStatement.ifPart.condition!!.replace(invertedCondition) + completeStatement.elsePart!!.statementList.replace(statement.ifPart.statementList) + val newStatement = statement.replace(completeStatement) as PyIfStatement + + switchAttachedComments(newStatement, newStatement.elsePart!!) + switchInlineComments(newStatement) + + // Highlighting placeholder + val passStatementRange = newStatement.ifPart.statementList.statements[0].textRange + editor.caretModel.primaryCaret.setSelection(passStatementRange.startOffset, passStatementRange.endOffset) + } + + private fun invertIfStatementFollowup(project: Project, file: PsiFile, statement: PyIfStatement, terminableStatement: PyStatement) { + ConditionUtil.invertConditionalExpression(project, file, statement.ifPart.condition!!) + + val ifStatements = statement.ifPart.statementList + + // Switching statements + val parent = statement.parent + val ifFirstChild = ifStatements.firstChild + val ifLastChild = ifStatements.lastChild + + val followupFirstChild = statement.nextMovableSibling + val followupLastChild = parent.lastChild + + parent.addRange(ifFirstChild, ifLastChild) + if (followupFirstChild != null) { + ifStatements.addRange(followupFirstChild, followupLastChild) + } + + switchAttachedComments(statement, statement.nextMovableSibling!!) + + ifStatements.deleteChildRange(ifFirstChild, ifLastChild) + if (followupFirstChild != null) { + parent.deleteChildRange(followupFirstChild, followupLastChild) + } + + // Fixing terminations + val followupTerminator = if (statement.parent.lastSignificantChild.isTerminationStatement) + statement.parent.lastSignificantChild + else + null + if (followupTerminator != null && !followupTerminator.isValuableTerminationStatement(terminableStatement)) { + followupTerminator.delete() + statement.parent.trimTrailingWhiteSpace() + } + + if (!ifStatements.isTerminated && + statement.parent.lastSignificantChild != statement && + ifStatements.findTerminationStatement() == null) { + ifStatements.add(terminableStatement.createTerminationStatement(file, project)) + } + + CodeEditUtil.markToReformat(parent.node, true) + CodeEditUtil.markToReformat(ifStatements.node, true) + } + + private fun switchAttachedComments(statement: PyIfStatement, oppositeAnchor: PsiElement) { + val oppositeComments = oppositeAnchor.attachedComments + val ifComments = statement.attachedComments + + if (ifComments.isNotEmpty()) { + oppositeAnchor.parent.addRangeBefore(ifComments.first(), ifComments.last(), oppositeAnchor) + statement.parent.deleteChildRange(ifComments.first(), ifComments.last()) + } + + if (oppositeComments.isNotEmpty()) { + statement.parent.addRangeBefore(oppositeComments.first(), oppositeComments.last(), statement) + oppositeAnchor.parent.deleteChildRange(oppositeComments.first(), oppositeComments.last()) + } + } + + private fun switchInlineComments(statement: PyIfStatement) { + val ifPart = statement.ifPart + val elsePart = statement.elsePart ?: return + + val ifComment = ifPart.childComment + val elseComment = elsePart.childComment + if (ifComment?.isInlineServiceComment == true || elseComment?.isInlineServiceComment == true) { + return + } + + if (ifComment != null) { + val trailingColonNode = elsePart.statementList.node.prevSignificantNode!! + CodeEditUtil.addChild(elsePart.node, ifComment.node.copyElement(), trailingColonNode.treeNext) + ifComment.delete() + } + + if (elseComment != null) { + val trailingColonNode = ifPart.statementList.node.prevSignificantNode!! + CodeEditUtil.addChild(ifPart.node, elseComment.node.copyElement(), trailingColonNode.treeNext) + elseComment.delete() + } + } + + private val PyStatementList.isTerminated: Boolean + get() { + val controlFlow = ControlFlowCache.getControlFlow(parentsOfType().first()) + val currentInstruction = controlFlow.instructions.first { it.element == this } + var result = true + ControlFlowUtil.iterate(currentInstruction.num(), controlFlow.instructions, { instruction -> + when { + instruction == currentInstruction -> ControlFlowUtil.Operation.NEXT + instruction is ReadWriteInstruction -> ControlFlowUtil.Operation.NEXT + instruction.element == null || !instruction.element!!.parents.contains(this) -> { + result = false + ControlFlowUtil.Operation.BREAK + } + instruction.element.isTerminationStatement -> ControlFlowUtil.Operation.CONTINUE + else -> ControlFlowUtil.Operation.NEXT + } + }, false) + return result + } + + private fun PsiElement.findTerminableParent() = PsiTreeUtil.getParentOfType( + this, PyFunction::class.java, PyLoopStatement::class.java) + + private fun PsiElement.findTerminationStatement(): PsiElement? = children.firstOrNull { it.isTerminationStatement } + + private val PsiElement?.isTerminationStatement: Boolean + get() = this is PyReturnStatement || + this is PyRaiseStatement || + this is PyContinueStatement || + this is PyBreakStatement + + private fun PsiElement.isValuableTerminationStatement(terminableStatement: PyStatement): Boolean = + this is PyReturnStatement && (expression != null || terminableStatement !is PyFunction) || + this is PyRaiseStatement || + this is PyBreakStatement + + private fun PyStatement.createTerminationStatement(file: PsiFile, project: Project): PsiElement { + val level = LanguageLevel.forElement(file) + val generator = PyElementGenerator.getInstance(project) + return when (this) { + is PyFunction -> generator.createFromText(level, PyReturnStatement::class.java, "return") + is PyLoopStatement -> generator.createFromText(level, PyContinueStatement::class.java, "continue") + else -> throw IncorrectOperationException("${javaClass.name} is not a terminable statement") + } + } + + private val PsiElement.attachedComments: List + get() = PyPsiUtils.getPrecedingComments(this).takeWhile { !it.isAttachedServiceComment } + + private val PsiElement.childComment + get() = node.getChildren(null).filterIsInstance().firstOrNull() + + private val ASTNode.prevSignificantNode + get() = PyPsiUtils.skipSiblingsBackward(this, insignificantTokenSet) + + private val PsiElement.lastSignificantChild + get() = PyPsiUtils.getPrevNonCommentSibling(lastChild, false) + + private val PsiElement.nextMovableSibling + get() = siblings(withSelf = false).firstOrNull { + it !is PsiWhiteSpace && + (it !is PsiComment || it.isAttachedServiceComment) + } + + private val PsiComment.isAttachedServiceComment + get() = SuppressionUtil.isSuppressionComment(this) || isPylintComment + + private val PsiComment.isInlineServiceComment + get() = PyTypeHintGenerationUtil.isTypeHintComment(this) || isPylintComment + + private val PsiComment.isPylintComment + get() = text.startsWith(PYLINT_COMMENT_PREFIX) + + private fun PsiElement.trimTrailingWhiteSpace() { + var lastChild = lastChild + while (lastChild is PsiWhiteSpace) { + val prevSibling = lastChild.prevSibling + lastChild.delete() + lastChild = prevSibling + } + } + +} \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyNegateComparisonIntention.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyNegateComparisonIntention.java index 35951529b437..f9c52424415c 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyNegateComparisonIntention.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyNegateComparisonIntention.java @@ -8,38 +8,18 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyPsiBundle; -import com.jetbrains.python.PyTokenTypes; -import com.jetbrains.python.psi.*; +import com.jetbrains.python.codeInsight.ConditionUtil; +import com.jetbrains.python.psi.PyBinaryExpression; +import com.jetbrains.python.psi.PyFile; +import kotlin.Pair; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; -import java.util.Map; - /** * Created by IntelliJ IDEA. * Author: Alexey.Ivanov */ public class PyNegateComparisonIntention extends PyBaseIntentionAction { - private static final Map comparisonStrings = new HashMap<>(7); - private static final Map invertedComparasions = new HashMap<>(7); - static { - comparisonStrings.put(PyTokenTypes.LT, "<"); - comparisonStrings.put(PyTokenTypes.GT, ">"); - comparisonStrings.put(PyTokenTypes.EQEQ, "=="); - comparisonStrings.put(PyTokenTypes.LE, "<="); - comparisonStrings.put(PyTokenTypes.GE, ">="); - comparisonStrings.put(PyTokenTypes.NE, "!="); - comparisonStrings.put(PyTokenTypes.NE_OLD, "<>"); - - invertedComparasions.put(PyTokenTypes.LT, PyTokenTypes.GE); - invertedComparasions.put(PyTokenTypes.GT, PyTokenTypes.LE); - invertedComparasions.put(PyTokenTypes.EQEQ, PyTokenTypes.NE); - invertedComparasions.put(PyTokenTypes.LE, PyTokenTypes.GT); - invertedComparasions.put(PyTokenTypes.GE, PyTokenTypes.LT); - invertedComparasions.put(PyTokenTypes.NE, PyTokenTypes.EQEQ); - invertedComparasions.put(PyTokenTypes.NE_OLD, PyTokenTypes.EQEQ); - } @Override @NotNull @@ -55,47 +35,18 @@ public class PyNegateComparisonIntention extends PyBaseIntentionAction { PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); PyBinaryExpression binaryExpression = PsiTreeUtil.getParentOfType(element, PyBinaryExpression.class, false); - while (binaryExpression != null) { - PyElementType operator = binaryExpression.getOperator(); - if (comparisonStrings.containsKey(operator)) { - setText(PyPsiBundle.message("INTN.negate.comparison", comparisonStrings.get(operator), - comparisonStrings.get(invertedComparasions.get(operator)))); - return true; - } - binaryExpression = PsiTreeUtil.getParentOfType(binaryExpression, PyBinaryExpression.class); + Pair negation = ConditionUtil.findComparisonNegationOperators(binaryExpression); + if (negation != null) { + setText(PyPsiBundle.message("INTN.negate.comparison", negation.component1(), negation.component2())); + return true; } return false; } @Override public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { - PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); PyBinaryExpression binaryExpression = PsiTreeUtil.getParentOfType(element, PyBinaryExpression.class, false); - while (binaryExpression != null) { - PyElementType operator = binaryExpression.getOperator(); - if (comparisonStrings.containsKey(operator)) { - PsiElement parent = binaryExpression.getParent(); - while (parent instanceof PyParenthesizedExpression) { - parent = parent.getParent(); - } - - final PyElementType invertedOperator = invertedComparasions.get(binaryExpression.getOperator()); - PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); - final PyBinaryExpression newElement = elementGenerator - .createBinaryExpression(comparisonStrings.get(invertedOperator), binaryExpression.getLeftExpression(), - binaryExpression.getRightExpression()); - - if (parent instanceof PyPrefixExpression && ((PyPrefixExpression)parent).getOperator() == PyTokenTypes.NOT_KEYWORD) { - parent.replace(newElement); - } - else { - final LanguageLevel level = LanguageLevel.forElement(file); - binaryExpression.replace(elementGenerator.createExpressionFromText(level, "not " + newElement.getText())); - } - return; - } - binaryExpression = PsiTreeUtil.getParentOfType(binaryExpression, PyBinaryExpression.class); - } + ConditionUtil.negateComparisonExpression(project, file, binaryExpression); } } diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java index 57f6a0b5381a..ef6132a42c14 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/intentions/PyTypeHintGenerationUtil.java @@ -388,6 +388,10 @@ public final class PyTypeHintGenerationUtil { } } + public static boolean isTypeHintComment(PsiElement element) { + return element instanceof PsiComment && element.getText().startsWith(TYPE_COMMENT_PREFIX); + } + public static final class Pep484IncompatibleTypeException extends RuntimeException { public Pep484IncompatibleTypeException(String message) { super(message);