Apply De Morgan's laws to non-negated binary expressions

So #KT-18460 Fixed
This commit is contained in:
Dmitry Neverov
2017-06-16 12:28:04 +03:00
committed by Mikhail Glukhikh
parent af53a0ecd5
commit 44ce5f73f7
44 changed files with 193 additions and 128 deletions
+1 -1
View File
@@ -1102,7 +1102,7 @@
</intentionAction> </intentionAction>
<intentionAction> <intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ConvertNegatedExpressionWithDemorgansLawIntention</className> <className>org.jetbrains.kotlin.idea.intentions.ConvertBinaryExpressionWithDemorgansLawIntention</className>
<category>Kotlin</category> <category>Kotlin</category>
</intentionAction> </intentionAction>
@@ -19,46 +19,49 @@ package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import java.util.* import java.util.*
class ConvertNegatedExpressionWithDemorgansLawIntention : SelfTargetingOffsetIndependentIntention<KtPrefixExpression>(KtPrefixExpression::class.java, "DeMorgan Law") { class ConvertBinaryExpressionWithDemorgansLawIntention : SelfTargetingOffsetIndependentIntention<KtBinaryExpression>(KtBinaryExpression::class.java, "DeMorgan Law") {
override fun isApplicableTo(element: KtPrefixExpression): Boolean { override fun isApplicableTo(element: KtBinaryExpression): Boolean {
val prefixOperator = element.operationReference.getReferencedNameElementType() val expr = element.parentsWithSelf.takeWhile { it is KtBinaryExpression }.last() as KtBinaryExpression
if (prefixOperator != KtTokens.EXCL) return false
val parenthesizedExpression = element.baseExpression as? KtParenthesizedExpression when (expr.operationToken) {
val baseExpression = parenthesizedExpression?.expression as? KtBinaryExpression ?: return false
when (baseExpression.operationToken) {
KtTokens.ANDAND -> text = "Replace '&&' with '||'" KtTokens.ANDAND -> text = "Replace '&&' with '||'"
KtTokens.OROR -> text = "Replace '||' with '&&'" KtTokens.OROR -> text = "Replace '||' with '&&'"
else -> return false else -> return false
} }
return splitBooleanSequence(baseExpression) != null return splitBooleanSequence(expr) != null
} }
override fun applyTo(element: KtPrefixExpression, editor: Editor?) { override fun applyTo(element: KtBinaryExpression, editor: Editor?) {
applyTo(element) applyTo(element)
} }
fun applyTo(element: KtPrefixExpression) { fun applyTo(element: KtBinaryExpression) {
val parenthesizedExpression = element.baseExpression as KtParenthesizedExpression val expr = element.parentsWithSelf.takeWhile { it is KtBinaryExpression }.last() as KtBinaryExpression
val baseExpression = parenthesizedExpression.expression as KtBinaryExpression
val operatorText = when (baseExpression.operationToken) { val operatorText = when (expr.operationToken) {
KtTokens.ANDAND -> KtTokens.OROR.value KtTokens.ANDAND -> KtTokens.OROR.value
KtTokens.OROR -> KtTokens.ANDAND.value KtTokens.OROR -> KtTokens.ANDAND.value
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
val operands = splitBooleanSequence(baseExpression)!!.asReversed() val operands = splitBooleanSequence(expr)!!.asReversed()
val newExpression = KtPsiFactory(element).buildExpression { val newExpression = KtPsiFactory(expr).buildExpression {
appendExpressions(operands.map { it.negate() }, separator = operatorText) appendExpressions(operands.map { it.negate() }, separator = operatorText)
} }
element.replace(newExpression) val grandParentPrefix = expr.parent.parent as? KtPrefixExpression
val negated = expr.parent is KtParenthesizedExpression &&
grandParentPrefix?.operationReference?.getReferencedNameElementType() == KtTokens.EXCL
if (negated) {
grandParentPrefix?.replace(newExpression)
} else {
expr.replace(newExpression.negate())
}
} }
private fun splitBooleanSequence(expression: KtBinaryExpression): List<KtExpression>? { private fun splitBooleanSequence(expression: KtBinaryExpression): List<KtExpression>? {
@@ -25,6 +25,7 @@ import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.core.unblockDocument import org.jetbrains.kotlin.idea.core.unblockDocument
import org.jetbrains.kotlin.idea.util.CommentSaver import org.jetbrains.kotlin.idea.util.CommentSaver
import org.jetbrains.kotlin.idea.util.psi.patternMatching.matches import org.jetbrains.kotlin.idea.util.psi.patternMatching.matches
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
@@ -58,9 +59,15 @@ class InvertIfConditionIntention : SelfTargetingIntention<KtIfExpression>(KtIfEx
commentSaver.restore(commentRestoreRange) commentSaver.restore(commentRestoreRange)
val newIfCondition = newIf.condition val newIfCondition = newIf.condition
val simplifyIntention = ConvertNegatedExpressionWithDemorgansLawIntention() val simplifyIntention = ConvertBinaryExpressionWithDemorgansLawIntention()
if (newIfCondition is KtPrefixExpression && simplifyIntention.isApplicableTo(newIfCondition)) { (newIfCondition as? KtPrefixExpression)?.let {
simplifyIntention.applyTo(newIfCondition) //use De Morgan's law only for negated condition to not make it more complex
if (it.operationReference.getReferencedNameElementType() == KtTokens.EXCL) {
val binaryExpr = (it.baseExpression as? KtParenthesizedExpression)?.expression as? KtBinaryExpression
if (binaryExpr != null && simplifyIntention.isApplicableTo(binaryExpr)) {
simplifyIntention.applyTo(binaryExpr)
}
}
} }
editor?.apply { editor?.apply {
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.ConvertBinaryExpressionWithDemorgansLawIntention
@@ -3,5 +3,5 @@ object O {
fun bar(): Boolean = true fun bar(): Boolean = true
} }
fun foo(p1: Boolean, p2: Boolean) { fun foo(p1: Boolean, p2: Boolean) {
if (<caret>!(O.foo() || O.bar())) return if (!(<caret>O.foo() || O.bar())) return
} }
@@ -3,5 +3,5 @@ object O {
fun bar(): Boolean = true fun bar(): Boolean = true
} }
fun foo(p1: Boolean, p2: Boolean) { fun foo(p1: Boolean, p2: Boolean) {
if (<caret>!O.foo() && !O.bar()) return if (!O.foo() && !O.bar()) return
} }
@@ -3,5 +3,5 @@ operator fun String.not(): Boolean {
} }
fun foo(a: Boolean, b: Boolean) : Boolean { fun foo(a: Boolean, b: Boolean) : Boolean {
return !<caret>(!"" || b) return !(<caret>!"" || b)
} }
@@ -1,3 +1,3 @@
fun foo(a: Boolean, b: Boolean, c: Boolean) : Boolean { fun foo(a: Boolean, b: Boolean, c: Boolean) : Boolean {
return !<caret>(a || !b || c) return !(<caret>a || !b || c)
} }
@@ -1,3 +1,3 @@
fun foo(a: Boolean, b: Boolean) : Boolean { fun foo(a: Boolean, b: Boolean) : Boolean {
return !<caret>(a && !b) return !(<caret>a && !b)
} }
@@ -1,4 +1,5 @@
// IS_APPLICABLE: false // INTENTION_TEXT: Replace '&&' with '||'
fun foo(a: Boolean, b: Boolean) : Boolean { fun foo(a: Boolean, b: Boolean) : Boolean {
return <caret>a && b return <caret>a && b
} }
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '&&' with '||'
fun foo(a: Boolean, b: Boolean) : Boolean {
return !(!a || !b)
}
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '||' with '&&'
fun foo(a: Boolean, b: Boolean) : Boolean {
return <caret>a || b
}
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '||' with '&&'
fun foo(a: Boolean, b: Boolean) : Boolean {
return !(!a && !b)
}
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '||' with '&&'
fun foo(a: Boolean, b: Boolean) : Boolean {
return <caret>!a || !b
}
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '||' with '&&'
fun foo(a: Boolean, b: Boolean) : Boolean {
return !(a && b)
}
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '&&' with '||'
fun foo(a: Boolean, b: Boolean) : Boolean {
return <caret>!a && !b
}
@@ -0,0 +1,5 @@
// INTENTION_TEXT: Replace '&&' with '||'
fun foo(a: Boolean, b: Boolean) : Boolean {
return !(a || b)
}
@@ -3,5 +3,5 @@ operator fun Boolean.unaryPlus(): Boolean {
} }
fun foo(a: Boolean, b: Boolean) : Boolean { fun foo(a: Boolean, b: Boolean) : Boolean {
return !<caret>(+a || b) return !(<caret>+a || b)
} }
@@ -1 +0,0 @@
org.jetbrains.kotlin.idea.intentions.ConvertNegatedExpressionWithDemorgansLawIntention
@@ -1,6 +1,7 @@
// "Replace with safe (?.) call" "false" // "Replace with safe (?.) call" "false"
// ACTION: Add non-null asserted (!!) call // ACTION: Add non-null asserted (!!) call
// ACTION: Flip '>' // ACTION: Flip '>'
// ACTION: Replace '&&' with '||'
// ACTION: Replace overloaded operator with function call // ACTION: Replace overloaded operator with function call
// ACTION: Simplify boolean expression // ACTION: Simplify boolean expression
// ERROR: Operator call corresponds to a dot-qualified call 'w?.x.compareTo(42)' which is not allowed on a nullable receiver 'w?.x'. // ERROR: Operator call corresponds to a dot-qualified call 'w?.x.compareTo(42)' which is not allowed on a nullable receiver 'w?.x'.
@@ -4026,6 +4026,123 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
} }
} }
@TestMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConvertBinaryExpressionWithDemorgansLaw extends AbstractIntentionTest {
public void testAllFilesPresentInConvertBinaryExpressionWithDemorgansLaw() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("complexNegation1.kt")
public void testComplexNegation1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/complexNegation1.kt");
doTest(fileName);
}
@TestMetadata("complexNegation2.kt")
public void testComplexNegation2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/complexNegation2.kt");
doTest(fileName);
}
@TestMetadata("conjunctionNegation1.kt")
public void testConjunctionNegation1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/conjunctionNegation1.kt");
doTest(fileName);
}
@TestMetadata("conjunctionNegation2.kt")
public void testConjunctionNegation2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/conjunctionNegation2.kt");
doTest(fileName);
}
@TestMetadata("disjunctionNegation1.kt")
public void testDisjunctionNegation1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/disjunctionNegation1.kt");
doTest(fileName);
}
@TestMetadata("disjunctionNegation2.kt")
public void testDisjunctionNegation2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/disjunctionNegation2.kt");
doTest(fileName);
}
@TestMetadata("dontAddRedundantParenthesis.kt")
public void testDontAddRedundantParenthesis() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/dontAddRedundantParenthesis.kt");
doTest(fileName);
}
@TestMetadata("doubleNegation.kt")
public void testDoubleNegation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/doubleNegation.kt");
doTest(fileName);
}
@TestMetadata("inapplicableOperator.kt")
public void testInapplicableOperator() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/inapplicableOperator.kt");
doTest(fileName);
}
@TestMetadata("inapplicableTriple.kt")
public void testInapplicableTriple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/inapplicableTriple.kt");
doTest(fileName);
}
@TestMetadata("longMixedExpression.kt")
public void testLongMixedExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/longMixedExpression.kt");
doTest(fileName);
}
@TestMetadata("mixedExpression.kt")
public void testMixedExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/mixedExpression.kt");
doTest(fileName);
}
@TestMetadata("noNegationPrefix1.kt")
public void testNoNegationPrefix1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/noNegationPrefix1.kt");
doTest(fileName);
}
@TestMetadata("noNegationPrefix2.kt")
public void testNoNegationPrefix2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/noNegationPrefix2.kt");
doTest(fileName);
}
@TestMetadata("noNegationPrefix3.kt")
public void testNoNegationPrefix3() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/noNegationPrefix3.kt");
doTest(fileName);
}
@TestMetadata("noNegationPrefix4.kt")
public void testNoNegationPrefix4() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/noNegationPrefix4.kt");
doTest(fileName);
}
@TestMetadata("nonstandardPrefixOperator.kt")
public void testNonstandardPrefixOperator() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/nonstandardPrefixOperator.kt");
doTest(fileName);
}
@TestMetadata("retainedParens.kt")
public void testRetainedParens() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertBinaryExpressionWithDemorgansLaw/retainedParens.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced") @TestMetadata("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)
@@ -5001,105 +5118,6 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
} }
} }
@TestMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConvertNegatedExpressionWithDemorgansLaw extends AbstractIntentionTest {
public void testAllFilesPresentInConvertNegatedExpressionWithDemorgansLaw() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("complexNegation1.kt")
public void testComplexNegation1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/complexNegation1.kt");
doTest(fileName);
}
@TestMetadata("complexNegation2.kt")
public void testComplexNegation2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/complexNegation2.kt");
doTest(fileName);
}
@TestMetadata("conjunctionNegation1.kt")
public void testConjunctionNegation1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/conjunctionNegation1.kt");
doTest(fileName);
}
@TestMetadata("conjunctionNegation2.kt")
public void testConjunctionNegation2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/conjunctionNegation2.kt");
doTest(fileName);
}
@TestMetadata("disjunctionNegation1.kt")
public void testDisjunctionNegation1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/disjunctionNegation1.kt");
doTest(fileName);
}
@TestMetadata("disjunctionNegation2.kt")
public void testDisjunctionNegation2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/disjunctionNegation2.kt");
doTest(fileName);
}
@TestMetadata("dontAddRedundantParenthesis.kt")
public void testDontAddRedundantParenthesis() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/dontAddRedundantParenthesis.kt");
doTest(fileName);
}
@TestMetadata("doubleNegation.kt")
public void testDoubleNegation() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/doubleNegation.kt");
doTest(fileName);
}
@TestMetadata("inapplicableNormalExpression.kt")
public void testInapplicableNormalExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/inapplicableNormalExpression.kt");
doTest(fileName);
}
@TestMetadata("inapplicableOperator.kt")
public void testInapplicableOperator() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/inapplicableOperator.kt");
doTest(fileName);
}
@TestMetadata("inapplicableTriple.kt")
public void testInapplicableTriple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/inapplicableTriple.kt");
doTest(fileName);
}
@TestMetadata("longMixedExpression.kt")
public void testLongMixedExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/longMixedExpression.kt");
doTest(fileName);
}
@TestMetadata("mixedExpression.kt")
public void testMixedExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/mixedExpression.kt");
doTest(fileName);
}
@TestMetadata("nonstandardPrefixOperator.kt")
public void testNonstandardPrefixOperator() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/nonstandardPrefixOperator.kt");
doTest(fileName);
}
@TestMetadata("retainedParens.kt")
public void testRetainedParens() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertNegatedExpressionWithDemorgansLaw/retainedParens.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/intentions/convertObjectLiteralToClass") @TestMetadata("idea/testData/intentions/convertObjectLiteralToClass")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)