org.cqfn.diktat.ruleset.rules.chapter3.BooleanExpressionsRule.kt Maven / Gradle / Ivy
package org.cqfn.diktat.ruleset.rules.chapter3
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.ruleset.constants.Warnings.COMPLEX_BOOLEAN_EXPRESSION
import org.cqfn.diktat.ruleset.rules.DiktatRule
import org.cqfn.diktat.ruleset.utils.KotlinParser
import org.cqfn.diktat.ruleset.utils.findAllNodesWithCondition
import org.cqfn.diktat.ruleset.utils.logicalInfixMethodMapping
import org.cqfn.diktat.ruleset.utils.logicalInfixMethods
import com.bpodgursky.jbool_expressions.Expression
import com.bpodgursky.jbool_expressions.options.ExprOptions
import com.bpodgursky.jbool_expressions.parsers.ExprParser
import com.bpodgursky.jbool_expressions.parsers.TokenMapper
import com.bpodgursky.jbool_expressions.rules.DeMorgan
import com.bpodgursky.jbool_expressions.rules.DistributiveLaw
import com.bpodgursky.jbool_expressions.rules.Rule
import com.bpodgursky.jbool_expressions.rules.RuleList
import com.bpodgursky.jbool_expressions.rules.RulesHelper
import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.CONDITION
import com.pinterest.ktlint.core.ast.ElementType.PARENTHESIZED
import com.pinterest.ktlint.core.ast.ElementType.PREFIX_EXPRESSION
import com.pinterest.ktlint.core.ast.isLeaf
import com.pinterest.ktlint.core.ast.isPartOfComment
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtParenthesizedExpression
import org.jetbrains.kotlin.psi.KtPrefixExpression
import org.jetbrains.kotlin.psi.psiUtil.parents
/**
* Rule that checks if the boolean expression can be simplified.
*/
class BooleanExpressionsRule(configRules: List) : DiktatRule(
NAME_ID,
configRules,
listOf(COMPLEX_BOOLEAN_EXPRESSION)
) {
override fun logic(node: ASTNode) {
if (node.elementType == CONDITION) {
checkBooleanExpression(node)
}
}
@Suppress("TooGenericExceptionCaught")
private fun checkBooleanExpression(node: ASTNode) {
// This class is used to assign a variable name for every elementary boolean expression. It is required for jbool to operate.
val expressionsReplacement = ExpressionsReplacement()
val correctedExpression = formatBooleanExpressionAsString(node, expressionsReplacement)
if (expressionsReplacement.isEmpty()) {
// this happens, if we haven't found any expressions that can be simplified
return
}
// If there are method calls in conditions
val expr: Expression = try {
ExprParser.parse(correctedExpression, expressionsReplacement.orderedTokenMapper)
} catch (exc: RuntimeException) {
if (exc.message?.startsWith("Unrecognized!") == true) {
// this comes up if there is an unparsable expression (jbool doesn't have own exception type). For example a.and(b)
return
} else {
throw exc
}
}
val simplifiedExpression = RulesHelper.applySet(expr, allRules(), ExprOptions.noCaching())
if (expr != simplifiedExpression) {
COMPLEX_BOOLEAN_EXPRESSION.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) {
fixBooleanExpression(node, simplifiedExpression, expressionsReplacement)
}
}
}
/**
* Converts a complex boolean expression into a string representation, mapping each elementary expression to a letter token.
* These tokens are collected into [expressionsReplacement].
* For example:
* ```
* (a > 5 && b != 2) -> A & B
* (a > 5 || false) -> A | false
* (a > 5 || x.foo()) -> A | B
* ```
*
* @param node
* @param expressionsReplacement a special class for replacements expression->token
* @return formatted string representation of expression
*/
@Suppress("UnsafeCallOnNullableType", "ForbiddenComment")
internal fun formatBooleanExpressionAsString(node: ASTNode, expressionsReplacement: ExpressionsReplacement): String {
val (booleanBinaryExpressions, otherBinaryExpressions) = node.collectElementaryExpressions()
val logicalExpressions = otherBinaryExpressions.filter { otherBinaryExpression ->
// keeping only boolean expressions, keeping things like `a + b < 6` and excluding `a + b`
(otherBinaryExpression.psi as KtBinaryExpression).operationReference.text in logicalInfixMethods &&
// todo: support xor; for now skip all expressions that are nested in xor
otherBinaryExpression.parents()
.takeWhile { it != node }
.none { (it.psi as? KtBinaryExpression)?.isXorExpression() ?: false }
}
// Boolean expressions like `a > 5 && b < 7` or `x.isEmpty() || (y.isNotEmpty())` we convert to individual parts.
val elementaryBooleanExpressions = booleanBinaryExpressions
.map { it.psi as KtBinaryExpression }
.flatMap { listOf(it.left!!.node, it.right!!.node) }
.map {
// remove parentheses around expression, if there are any
it.removeAllParentheses()
}
.filterNot {
// finally, if parts are binary expressions themselves, they should be present in our lists and we will process them later.
it.elementType == BINARY_EXPRESSION ||
// !(a || b) should be skipped too, `a` and `b` should be present later
(it.psi as? KtPrefixExpression)?.lastChild
?.node
?.removeAllParentheses()
?.elementType == BINARY_EXPRESSION ||
// `true` and `false` are valid tokens for jBool, so we keep them.
it.text == "true" || it.text == "false"
}
(logicalExpressions + elementaryBooleanExpressions).forEach { expression ->
expressionsReplacement.addExpression(expression)
}
// Prepare final formatted string
// At first, substitute all elementary expressions with variables
val correctedExpression = expressionsReplacement.replaceExpressions(node.textWithoutComments())
// jBool library is using & as && and | as ||
return "(${correctedExpression
.replace("&&", "&")
.replace("||", "|")})"
}
/**
* Split the complex expression into elementary parts
*/
private fun ASTNode.collectElementaryExpressions() = this
.findAllNodesWithCondition { astNode ->
astNode.elementType == BINARY_EXPRESSION &&
// filter out boolean conditions in nested lambdas, e.g. `if (foo.filter { a && b })`
(astNode == this || astNode.parents().takeWhile { it != this }
.all { it.elementType in setOf(BINARY_EXPRESSION, PARENTHESIZED, PREFIX_EXPRESSION) })
}
.partition {
val operationReferenceText = (it.psi as KtBinaryExpression).operationReference.text
operationReferenceText == "&&" || operationReferenceText == "||"
}
private fun ASTNode.removeAllParentheses(): ASTNode {
val result = (this.psi as? KtParenthesizedExpression)?.expression?.node ?: return this
return result.removeAllParentheses()
}
private fun ASTNode.textWithoutComments() = findAllNodesWithCondition(withSelf = false) {
it.isLeaf()
}
.filterNot { it.isPartOfComment() }
.joinToString(separator = "") { it.text }
.replace("\n", " ")
private fun fixBooleanExpression(
node: ASTNode,
simplifiedExpr: Expression,
expressionsReplacement: ExpressionsReplacement
) {
val correctKotlinBooleanExpression = simplifiedExpr
.toString()
.replace("&", "&&")
.replace("|", "||")
.removePrefix("(")
.removeSuffix(")")
node.replaceChild(node.firstChildNode,
KotlinParser().createNode(expressionsReplacement.restoreFullExpression(correctKotlinBooleanExpression)))
}
private fun KtBinaryExpression.isXorExpression() = operationReference.text == "xor"
/**
* A special class to replace expressions (and restore it back)
* Note: mapping is String to Char(and Char to Char) actually, but will keep it as String for simplicity
*/
internal inner class ExpressionsReplacement {
private val expressionToToken: HashMap = LinkedHashMap()
private val tokenToExpression: HashMap = HashMap()
private val tokenToOrderedToken: HashMap = HashMap()
/**
* TokenMapper for first call ExprParser which remembers the order of expression.
*/
val orderedTokenMapper: TokenMapper = TokenMapper { name -> getLetter(tokenToOrderedToken, name) }
/**
* Returns true if this object contains no replacements.
*
* @return true if this object contains no replacements
*/
fun isEmpty(): Boolean = expressionToToken.isEmpty()
/**
* Returns the number of replacements in this object.
*
* @return the number of replacements in this object
*/
fun size(): Int = expressionToToken.size
/**
* Register an expression for further replacement
*
* @param expressionAstNode astNode which contains boolean expression
*/
fun addExpression(expressionAstNode: ASTNode) {
val expressionText = expressionAstNode.textWithoutComments()
// support case when `boolean_expression` matches to `!boolean_expression`
val (expression, negativeExpression) = if (expressionText.startsWith('!')) {
expressionText.substring(1) to expressionText
} else {
expressionText to getNegativeExpression(expressionAstNode, expressionText)
}
val letter = getLetter(expressionToToken, expression)
tokenToExpression["!$letter"] = negativeExpression
tokenToExpression[letter] = expression
}
/**
* Replaces registered expressions in provided expression
*
* @param fullExpression full boolean expression in kotlin
* @return full expression in jbool format
*/
fun replaceExpressions(fullExpression: String): String {
var resultExpression = fullExpression
expressionToToken.keys
.sortedByDescending { it.length }
.forEach { refExpr ->
resultExpression = resultExpression.replace(refExpr, expressionToToken.getValue(refExpr))
}
return resultExpression
}
/**
* Restores full expression by replacing tokens and restoring the order
*
* @param fullExpression full boolean expression in jbool format
* @return full boolean expression in kotlin
*/
fun restoreFullExpression(fullExpression: String): String {
// restore order
var resultExpression = fullExpression
tokenToOrderedToken.values.forEachIndexed { index, value ->
resultExpression = resultExpression.replace(value, "%${index + 1}\$s")
}
resultExpression = resultExpression.format(args = tokenToOrderedToken.keys.toTypedArray())
// restore expression
tokenToExpression.keys.forEachIndexed { index, value ->
resultExpression = resultExpression.replace(value, "%${index + 1}\$s")
}
resultExpression = resultExpression.format(args = tokenToExpression.values.toTypedArray())
return resultExpression
}
private fun getLetter(letters: HashMap, key: String) = letters
.computeIfAbsent(key) {
('A'.code + letters.size).toChar().toString()
}
private fun getNegativeExpression(expressionAstNode: ASTNode, expression: String): String =
if (expressionAstNode.elementType == BINARY_EXPRESSION) {
val operation = (expressionAstNode.psi as KtBinaryExpression).operationReference.text
logicalInfixMethodMapping[operation]?.let {
expression.replace(operation, it)
} ?: "!($expression)"
} else {
"!$expression"
}
}
companion object {
const val NAME_ID = "boolean-expressions-rule"
private fun allRules(): RuleList {
val rules: MutableList> = ArrayList(RulesHelper.simplifyRules().rules)
rules.add(DeMorgan())
rules.add(DistributiveLaw())
return RuleList(rules)
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy