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);