org.cqfn.diktat.ruleset.rules.chapter4.SmartCastRule.kt Maven / Gradle / Ivy
package org.cqfn.diktat.ruleset.rules.chapter4
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.ruleset.constants.Warnings.SMART_CAST_NEEDED
import org.cqfn.diktat.ruleset.rules.DiktatRule
import org.cqfn.diktat.ruleset.utils.KotlinParser
import org.cqfn.diktat.ruleset.utils.findAllDescendantsWithSpecificType
import org.cqfn.diktat.ruleset.utils.findParentNodeWithSpecificType
import org.cqfn.diktat.ruleset.utils.getFirstChildWithType
import org.cqfn.diktat.ruleset.utils.hasParent
import org.cqfn.diktat.ruleset.utils.search.findAllVariablesWithUsages
import com.pinterest.ktlint.core.ast.ElementType.BINARY_WITH_TYPE
import com.pinterest.ktlint.core.ast.ElementType.BLOCK
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.ELSE
import com.pinterest.ktlint.core.ast.ElementType.FILE
import com.pinterest.ktlint.core.ast.ElementType.FLOAT_CONSTANT
import com.pinterest.ktlint.core.ast.ElementType.IF
import com.pinterest.ktlint.core.ast.ElementType.INTEGER_CONSTANT
import com.pinterest.ktlint.core.ast.ElementType.IS_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE
import com.pinterest.ktlint.core.ast.ElementType.THEN
import com.pinterest.ktlint.core.ast.ElementType.WHEN
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.parents
/**
* Rule that detects redundant explicit casts
*/
class SmartCastRule(configRules: List) : DiktatRule(
NAME_ID,
configRules,
listOf(SMART_CAST_NEEDED)
) {
override fun logic(node: ASTNode) {
if (node.elementType == FILE) {
val usages = collectLocalPropertiesWithUsages(node)
val properMap = collectReferenceList(usages)
handleProp(properMap)
}
if (node.elementType == WHEN) {
// Rule is simplified after https://github.com/saveourtool/diktat/issues/1168
return
}
}
// Divide in is and as expr
@Suppress("TYPE_ALIAS")
private fun handleProp(propMap: Map>) {
propMap.forEach { (property, references) ->
val isExpr: MutableList = mutableListOf()
val asExpr: MutableList = mutableListOf()
references.forEach {
if (it.node.hasParent(IS_EXPRESSION)) {
isExpr.add(it)
} else if (it.node.hasParent(BINARY_WITH_TYPE) && it.node.treeParent.text.contains(KtTokens.AS_KEYWORD.value)) {
asExpr.add(it)
}
}
val groups = groupIsAndAsExpr(isExpr, asExpr, property)
if (groups.isNotEmpty()) {
handleGroups(groups)
}
}
}
/**
* If condition == is then we are looking for then block
* If condition == !is then we are looking for else block
*/
@Suppress("NestedBlockDepth", "TYPE_ALIAS")
private fun handleGroups(groups: Map>) {
groups.keys.forEach { key ->
val parentText = key.node.treeParent.text
if (parentText.contains(" is ")) {
groups.getValue(key).forEach { asCall ->
if (asCall.node.hasParent(THEN)) {
raiseWarning(asCall.node)
}
}
} else if (parentText.contains(" !is ")) {
groups.getValue(key).forEach { asCall ->
if (asCall.node.hasParent(ELSE)) {
raiseWarning(asCall.node)
}
}
}
}
}
@Suppress("UnsafeCallOnNullableType")
private fun raiseWarning(asCall: ASTNode) {
SMART_CAST_NEEDED.warnAndFix(configRules, emitWarn, isFixMode, asCall.treeParent.text, asCall.startOffset,
asCall) {
val dotExpr = asCall.findParentNodeWithSpecificType(DOT_QUALIFIED_EXPRESSION)
val afterDotPart = dotExpr?.text?.split(".")?.get(1)
val text = afterDotPart?.let { "${asCall.text}.$afterDotPart" } ?: asCall.text
(dotExpr ?: asCall.treeParent).treeParent.addChild(KotlinParser().createNode(text), (dotExpr ?: asCall.treeParent))
(dotExpr ?: asCall.treeParent).treeParent.removeChild((dotExpr ?: asCall.treeParent))
}
}
/**
* Groups is and as expressions, so that they are used in same if block
*/
@Suppress("TYPE_ALIAS")
private fun groupIsAndAsExpr(isExpr: List,
asExpr: List,
prop: KtProperty
): Map> {
if (isExpr.isEmpty() && asExpr.isNotEmpty()) {
handleZeroIsCase(asExpr, prop)
return emptyMap()
}
val groupedExprs: MutableMap> = mutableMapOf()
isExpr.forEach {
val list: MutableList = mutableListOf()
asExpr.forEach { asCall ->
if (asCall.node.findParentNodeWithSpecificType(IF)
== it.node.findParentNodeWithSpecificType(IF)) {
list.add(asCall)
}
}
groupedExprs[it] = list
}
return groupedExprs
}
@Suppress("UnsafeCallOnNullableType")
private fun getPropertyType(prop: KtProperty): String? {
when (prop.initializer?.node?.elementType) {
STRING_TEMPLATE -> return "String"
INTEGER_CONSTANT -> return "Int"
else -> {
}
}
if (prop.initializer?.node?.elementType == FLOAT_CONSTANT) {
if (prop.initializer?.text!!.endsWith("f")) {
return "Float"
}
return "Double"
}
return null
}
@Suppress("UnsafeCallOnNullableType")
private fun handleZeroIsCase(asExpr: List, prop: KtProperty) {
val propType = getPropertyType(prop) ?: return
asExpr
.map { it.node }
.forEach {
if (it.treeParent.text.endsWith(propType)) {
raiseWarning(it)
}
}
}
private fun handleThenBlock(then: ASTNode, blocks: List) {
val thenBlock = then.findChildByType(BLOCK)
thenBlock?.let {
// Find all as expressions that are inside this current block
val asList = thenBlock
.findAllDescendantsWithSpecificType(BINARY_WITH_TYPE)
.filter {
it.text.contains(" as ") && it.findParentNodeWithSpecificType(BLOCK) == thenBlock
}
.filterNot { (it.getFirstChildWithType(REFERENCE_EXPRESSION)?.psi as KtNameReferenceExpression).getLocalDeclaration() != null }
checkAsExpressions(asList, blocks)
}
?: run {
val asList = then.findAllDescendantsWithSpecificType(BINARY_WITH_TYPE).filter { it.text.contains(KtTokens.AS_KEYWORD.value) }
checkAsExpressions(asList, blocks)
}
}
@Suppress("UnsafeCallOnNullableType")
private fun checkAsExpressions(asList: List, blocks: List) {
val asExpr: MutableList = mutableListOf()
asList.forEach {
val split = it.text.split("as").map { part -> part.trim() }
asExpr.add(AsExpressions(split[0], split[1], it))
}
val exprToChange = asExpr.filter {
blocks.any { isExpr ->
isExpr.identifier == it.identifier &&
isExpr.type == it.type
}
}
if (exprToChange.isNotEmpty()) {
exprToChange.forEach {
SMART_CAST_NEEDED.warnAndFix(configRules, emitWarn, isFixMode, "${it.identifier} as ${it.type}", it.node.startOffset,
it.node) {
val dotExpr = it.node.findParentNodeWithSpecificType(DOT_QUALIFIED_EXPRESSION)!!
val afterDotPart = dotExpr.text.split(".")[1]
val text = "${it.identifier}.$afterDotPart"
dotExpr.treeParent.addChild(KotlinParser().createNode(text), dotExpr)
dotExpr.treeParent.removeChild(dotExpr)
}
}
}
}
private fun KtNameReferenceExpression.getLocalDeclaration(): KtProperty? = parents
.mapNotNull { it as? KtBlockExpression }
.first()
.let { blockExpression ->
blockExpression
.statements
.takeWhile { !it.isAncestor(this, true) }
.mapNotNull { it as? KtProperty }
.find {
it.isLocal &&
it.hasInitializer() &&
it.name?.equals(getReferencedName())
?: false
}
}
private fun collectLocalPropertiesWithUsages(node: ASTNode) = node
.findAllVariablesWithUsages { propertyNode ->
propertyNode.name != null
}
/**
* Gets references, which contains is or as keywords
*
* @return Map of property and list of expressions
*/
@Suppress("TYPE_ALIAS")
private fun collectReferenceList(propertiesToUsages: Map>): Map> =
propertiesToUsages.mapValues { (_, value) ->
value.filter { entry ->
entry.parent.node.elementType == IS_EXPRESSION || entry.parent.node.elementType == BINARY_WITH_TYPE
}
}
/**
* @property identifier a reference that is cast
* @property type a type to which the reference is being cast
* @property node a node that holds the entire expression
*/
data class AsExpressions(
val identifier: String,
val type: String,
val node: ASTNode
)
/**
* @property identifier a reference that is checked
* @property type a type with which the reference is being compared
*/
data class IsExpressions(val identifier: String, val type: String)
companion object {
const val NAME_ID = "smart-cast-rule"
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy