All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cqfn.diktat.ruleset.rules.chapter3.CollapseIfStatementsRule.kt Maven / Gradle / Ivy

There is a newer version: 1.2.5
Show newest version
package org.cqfn.diktat.ruleset.rules.chapter3

import org.cqfn.diktat.common.config.rules.RuleConfiguration
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.common.config.rules.getRuleConfig
import org.cqfn.diktat.ruleset.constants.Warnings
import org.cqfn.diktat.ruleset.rules.DiktatRule
import org.cqfn.diktat.ruleset.utils.KotlinParser

import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.IF
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.THEN
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.children
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtIfExpression

import java.util.Stack

typealias PlaceOfWarningForCurrentNode = Pair

/**
 * Rule for redundant nested if-statements, which could be collapsed into a single one
 */
class CollapseIfStatementsRule(configRules: List) : DiktatRule(
    NAME_ID,
    configRules,
    listOf(
        Warnings.COLLAPSE_IF_STATEMENTS
    )
) {
    private val configuration by lazy {
        CollapseIfStatementsConfiguration(
            configRules.getRuleConfig(Warnings.COLLAPSE_IF_STATEMENTS)?.configuration ?: emptyMap()
        )
    }

    // We hold the warnings, which we raised, since in case of multi nested if-statement,
    // there are could be several identical warning for one line
    private val listOfWarnings: MutableSet = mutableSetOf()

    override fun logic(node: ASTNode) {
        if (node.elementType == IF) {
            process(node)
        }
    }

    private fun process(node: ASTNode) {
        val startCollapseFromLevel = configuration.startCollapseFromNestedLevel
        val listOfNestedNodes: Stack = Stack()

        var nestedIfNode = findNestedIf(node)
        while (nestedIfNode != null) {
            listOfNestedNodes.push(nestedIfNode)
            nestedIfNode = findNestedIf(nestedIfNode)
        }
        val nestedLevel = listOfNestedNodes.size + 1
        if (nestedLevel < startCollapseFromLevel) {
            return
        }
        while (listOfNestedNodes.isNotEmpty()) {
            val currNode = listOfNestedNodes.pop()
            // Since the external `if` statement is not the direct parent,
            // we need multiple steps to take the required one
            // BLOCK -> THEN -> IF
            val currParentNode = currNode.treeParent.treeParent.treeParent
            if (listOfWarnings.add(currNode.startOffset to currNode)) {
                Warnings.COLLAPSE_IF_STATEMENTS.warnAndFix(
                    configRules, emitWarn, isFixMode,
                    "avoid using redundant nested if-statements", currNode.startOffset, currNode
                ) {
                    collapse(currParentNode, currNode)
                }
            }
        }
    }

    private fun findNestedIf(parentNode: ASTNode): ASTNode? {
        val parentThenNode = (parentNode.psi as KtIfExpression).then?.node ?: return null
        val nestedIfNode = parentThenNode.findChildByType(IF) ?: return null
        // We won't collapse if-statements, if some of them have `else` node
        if ((parentNode.psi as KtIfExpression).`else` != null ||
                (nestedIfNode.psi as KtIfExpression).`else` != null) {
            return null
        }
        // We monitor which types of nodes are followed before and after nested `if`
        // and we allow only a limited number of types to pass through.
        // Otherwise discovered `if` is not nested
        // We don't expect KDOC in `if-statements`, since it's a bad practise, and such code by meaning of our
        // code analyzer is invalid
        // However, if in some case we will hit the KDOC, than we won't collapse statements
        val listOfNodesBeforeNestedIf = parentThenNode.getChildren(null).takeWhile { it.elementType != IF }
        val listOfNodesAfterNestedIf = parentThenNode.getChildren(null).takeLastWhile { it != parentThenNode.findChildByType(IF) }
        val allowedTypes = listOf(LBRACE, WHITE_SPACE, RBRACE, BLOCK_COMMENT, EOL_COMMENT)
        if (listOfNodesBeforeNestedIf.any { it.elementType !in allowedTypes } ||
                listOfNodesAfterNestedIf.any { it.elementType !in allowedTypes }) {
            return null
        }
        return nestedIfNode
    }

    private fun takeCommentsBeforeNestedIf(node: ASTNode): List {
        val thenNode = (node.psi as KtIfExpression).then?.node
        return thenNode?.children()?.takeWhile { it.elementType != IF }?.filter {
            it.elementType == EOL_COMMENT ||
                    it.elementType == BLOCK_COMMENT
        }
            ?.toList() ?: emptyList()
    }

    private fun collapse(parentNode: ASTNode, nestedNode: ASTNode) {
        collapseConditions(parentNode, nestedNode)
        collapseThenBlocks(parentNode, nestedNode)
    }

    private fun collapseConditions(parentNode: ASTNode, nestedNode: ASTNode) {
        // If there are comments before nested if, we will move them into parent condition
        val comments = takeCommentsBeforeNestedIf(parentNode)
        val commentsText = if (comments.isNotEmpty()) {
            comments.joinToString(prefix = "\n", postfix = "\n", separator = "\n") { it.text }
        } else {
            " "
        }
        // Merge parent and nested conditions
        val parentConditionText = extractConditions(parentNode)
        val nestedCondition = (nestedNode.psi as KtIfExpression).condition
        val nestedConditionText = extractConditions(nestedNode)
        // If the nested condition is compound,
        // we need to put it to the brackets, according algebra of logic
        val mergeCondition =
                if (nestedCondition?.node?.elementType == BINARY_EXPRESSION &&
                        nestedCondition.node?.findChildByType(OPERATION_REFERENCE)?.text == "||"
                ) {
                    "if ($parentConditionText &&$commentsText($nestedConditionText)) {}"
                } else {
                    "if ($parentConditionText &&$commentsText$nestedConditionText) {}"
                }
        val newParentIfNode = KotlinParser().createNode(mergeCondition)
        // Remove THEN block
        newParentIfNode.removeChild(newParentIfNode.lastChildNode)
        // Remove old `if` from parent
        parentNode.removeRange(parentNode.firstChildNode, parentNode.findChildByType(THEN))
        // Add to parent all child from new `if` node
        var addAfter = parentNode.firstChildNode
        newParentIfNode.getChildren(null).forEachIndexed { index, child ->
            parentNode.addChild(child, addAfter)
            addAfter = parentNode.children().drop(index + 1).first()
        }
    }

    // If condition contains comments, we need additional actions
    // Because of `node.condition` will ignore comments
    private fun extractConditions(node: ASTNode): String {
        val condition = node.getChildren(null)
            .takeLastWhile { it != node.findChildByType(LPAR) }
            .takeWhile { it != node.findChildByType(RPAR) }
        return condition.joinToString("") { it.text }
    }

    private fun collapseThenBlocks(parentNode: ASTNode, nestedNode: ASTNode) {
        // Remove comments from parent node, since we already moved them into parent condition
        val comments = takeCommentsBeforeNestedIf(parentNode)
        comments.forEach {
            if (it.treeNext.elementType == WHITE_SPACE &&
                    it.treePrev.elementType == WHITE_SPACE) {
                parentNode.removeChild(it.treePrev)
            }
            parentNode.removeChild(it)
        }
        // Merge parent and nested `THEN` blocks
        val nestedThenNode = (nestedNode.psi as KtIfExpression).then
        val nestedContent = (nestedThenNode as KtBlockExpression).children().toMutableList()
        // Remove {, }, and white spaces
        repeat(2) {
            val firstElType = nestedContent.first().elementType
            if (firstElType == WHITE_SPACE ||
                    firstElType == LBRACE) {
                nestedContent.removeFirst()
            }
            val lastElType = nestedContent.last().elementType
            if (lastElType == WHITE_SPACE ||
                    lastElType == RBRACE) {
                nestedContent.removeLast()
            }
        }
        val nestedThenText = nestedContent.joinToString("") { it.text }
        val newNestedNode = KotlinParser().createNode(nestedThenText).treeParent
        val parentThenNode = (parentNode.psi as KtIfExpression).then?.node
        newNestedNode.getChildren(null).forEach {
            parentThenNode?.addChild(it, nestedNode)
        }
        parentThenNode?.removeChild(nestedNode)
    }

    /**
     * [RuleConfiguration] for configuration
     */
    class CollapseIfStatementsConfiguration(config: Map) : RuleConfiguration(config) {
        /**
         *  Collapse statements only if nested level more than this value
         */
        val startCollapseFromNestedLevel = config["startCollapseFromNestedLevel"]?.toInt() ?: DEFAULT_NESTED_LEVEL
    }

    companion object {
        private const val DEFAULT_NESTED_LEVEL = 2
        const val NAME_ID = "abu-collapse-if"
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy