Refactoring of IfThen: ToDoubleBang / ToElvis / ToSafeAccess
Some common actions were extracted to IfThenUtils.kt Use consistent logic in all three intentions Also fixes potential PSI consistency problems in conversions dot->safe calls
This commit is contained in:
@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.core.replaced
|
||||
import org.jetbrains.kotlin.idea.core.setType
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
@@ -248,19 +249,19 @@ fun KtDotQualifiedExpression.replaceFirstReceiver(
|
||||
newReceiver: KtExpression,
|
||||
safeAccess: Boolean = false
|
||||
): KtExpression {
|
||||
val receiver = receiverExpression
|
||||
if (safeAccess) {
|
||||
operationTokenNode.psi.replace(factory.createSafeCallNode().psi)
|
||||
}
|
||||
val replaced = (if (safeAccess) {
|
||||
this.replaced(factory.createExpressionByPattern("$0?.$1", receiverExpression, selectorExpression!!))
|
||||
} else this) as KtQualifiedExpression
|
||||
val receiver = replaced.receiverExpression
|
||||
when (receiver) {
|
||||
is KtDotQualifiedExpression -> {
|
||||
receiver.replaceFirstReceiver(factory, newReceiver, safeAccess)
|
||||
receiver.replace(receiver.replaceFirstReceiver(factory, newReceiver, safeAccess))
|
||||
}
|
||||
else -> {
|
||||
receiver.replace(newReceiver)
|
||||
}
|
||||
}
|
||||
return this
|
||||
return replaced
|
||||
}
|
||||
|
||||
fun KtDotQualifiedExpression.deleteFirstReceiver(): KtExpression {
|
||||
|
||||
+92
-2
@@ -23,6 +23,8 @@ import org.jetbrains.kotlin.KtNodeTypes
|
||||
import org.jetbrains.kotlin.descriptors.VariableDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.core.replaced
|
||||
import org.jetbrains.kotlin.idea.intentions.getLeftMostReceiverExpression
|
||||
import org.jetbrains.kotlin.idea.intentions.replaceFirstReceiver
|
||||
import org.jetbrains.kotlin.idea.refactoring.inline.KotlinInlineValHandler
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
@@ -31,8 +33,11 @@ import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.BindingContextUtils
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
|
||||
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowValueFactory
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.constant
|
||||
|
||||
fun KtBinaryExpression.expressionComparedToNull(): KtExpression? {
|
||||
@@ -62,8 +67,7 @@ fun KtExpression?.isNullExpression(): Boolean = this?.unwrapBlockOrParenthesis()
|
||||
fun KtExpression?.isNullExpressionOrEmptyBlock(): Boolean = this.isNullExpression() || this is KtBlockExpression && this.statements.isEmpty()
|
||||
|
||||
fun KtThrowExpression.throwsNullPointerExceptionWithNoArguments(): Boolean {
|
||||
val thrownExpression = this.thrownExpression
|
||||
if (thrownExpression !is KtCallExpression) return false
|
||||
val thrownExpression = this.thrownExpression as? KtCallExpression ?: return false
|
||||
|
||||
val context = this.analyze(BodyResolveMode.PARTIAL)
|
||||
val nameExpression = thrownExpression.calleeExpression as? KtNameReferenceExpression ?: return false
|
||||
@@ -135,3 +139,89 @@ fun KtExpression.isStableVariable(context: BindingContext = this.analyze()): Boo
|
||||
return descriptor is VariableDescriptor &&
|
||||
DataFlowValueFactory.isStableValue(descriptor, DescriptorUtils.getContainingModule(descriptor))
|
||||
}
|
||||
|
||||
data class IfThenToSelectData(
|
||||
val context: BindingContext,
|
||||
val condition: KtOperationExpression,
|
||||
val receiverExpression: KtExpression,
|
||||
val baseClause: KtExpression?,
|
||||
val negatedClause: KtExpression?
|
||||
) {
|
||||
internal fun replacedBaseClause(factory: KtPsiFactory): KtExpression {
|
||||
baseClause ?: error("Base clause must be not-null here")
|
||||
val newReceiver = (condition as? KtIsExpression)?.let {
|
||||
factory.createExpressionByPattern("$0 as? $1",
|
||||
(baseClause as? KtDotQualifiedExpression)?.getLeftMostReceiverExpression() ?: baseClause,
|
||||
it.typeReference!!)
|
||||
}
|
||||
return if (baseClause.evaluatesTo(receiverExpression)) {
|
||||
if (condition is KtIsExpression) newReceiver!! else baseClause
|
||||
}
|
||||
else {
|
||||
if (condition is KtIsExpression) {
|
||||
(baseClause as KtDotQualifiedExpression).replaceFirstReceiver(
|
||||
factory, newReceiver!!, safeAccess = true)
|
||||
}
|
||||
else {
|
||||
baseClause.insertSafeCalls(factory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun KtIfExpression.buildSelectTransformationData(): IfThenToSelectData? {
|
||||
val context = analyze()
|
||||
|
||||
val condition = condition as? KtOperationExpression ?: return null
|
||||
val thenClause = then?.unwrapBlockOrParenthesis()
|
||||
val elseClause = `else`?.unwrapBlockOrParenthesis()
|
||||
val receiverExpression = condition.checkedExpression() ?: return null
|
||||
|
||||
val (baseClause, negatedClause) = when (condition) {
|
||||
is KtBinaryExpression -> when (condition.operationToken) {
|
||||
KtTokens.EQEQ -> elseClause to thenClause
|
||||
KtTokens.EXCLEQ -> thenClause to elseClause
|
||||
else -> return null
|
||||
}
|
||||
is KtIsExpression -> {
|
||||
val targetType = context[BindingContext.TYPE, condition.typeReference] ?: return null
|
||||
if (TypeUtils.isNullableType(targetType)) return null
|
||||
// TODO: the following check can be removed after fix of KT-14576
|
||||
val originalType = receiverExpression.getType(context) ?: return null
|
||||
if (!targetType.isSubtypeOf(originalType)) return null
|
||||
|
||||
when (condition.isNegated) {
|
||||
true -> elseClause to thenClause
|
||||
false -> thenClause to elseClause
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
return IfThenToSelectData(context, condition, receiverExpression, baseClause, negatedClause)
|
||||
}
|
||||
|
||||
private fun KtExpression.checkedExpression() = when (this) {
|
||||
is KtBinaryExpression -> expressionComparedToNull()
|
||||
is KtIsExpression -> leftHandSide
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal fun KtExpression.hasNullableType(context: BindingContext): Boolean {
|
||||
val type = getType(context) ?: return true
|
||||
return TypeUtils.isNullableType(type)
|
||||
}
|
||||
|
||||
internal fun KtExpression.hasFirstReceiverOf(receiver: KtExpression): Boolean {
|
||||
val actualReceiver = (this as? KtDotQualifiedExpression)?.getLeftMostReceiverExpression() ?: return false
|
||||
return actualReceiver.evaluatesTo(receiver)
|
||||
}
|
||||
|
||||
private fun KtExpression.insertSafeCalls(factory: KtPsiFactory): KtExpression {
|
||||
if (this !is KtQualifiedExpression) return this
|
||||
val replaced = (if (this is KtDotQualifiedExpression) {
|
||||
this.replaced(factory.createExpressionByPattern("$0?.$1", receiverExpression, selectorExpression!!))
|
||||
} else this) as KtQualifiedExpression
|
||||
replaced.receiverExpression.let { it.replace(it.insertSafeCalls(factory)) }
|
||||
return replaced
|
||||
}
|
||||
|
||||
+12
-30
@@ -18,43 +18,26 @@ package org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
|
||||
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsStatement
|
||||
|
||||
class IfThenToDoubleBangIntention : SelfTargetingRangeIntention<KtIfExpression>(KtIfExpression::class.java, "Replace 'if' expression with '!!' expression") {
|
||||
class IfThenToDoubleBangIntention : SelfTargetingRangeIntention<KtIfExpression>(
|
||||
KtIfExpression::class.java, "Replace 'if' expression with '!!' expression"
|
||||
) {
|
||||
override fun applicabilityRange(element: KtIfExpression): TextRange? {
|
||||
val condition = element.condition as? KtBinaryExpression ?: return null
|
||||
val thenClause = element.then ?: return null
|
||||
val elseClause = element.`else`
|
||||
val (context, condition, receiverExpression, baseClause, negatedClause) = element.buildSelectTransformationData() ?: return null
|
||||
// TODO: here "Replace with as" can be supported
|
||||
if (condition is KtIsExpression) return null
|
||||
|
||||
val expression = condition.expressionComparedToNull() ?: return null
|
||||
val throwExpression = negatedClause as? KtThrowExpression ?: return null
|
||||
|
||||
val token = condition.operationToken
|
||||
|
||||
val throwExpression: KtThrowExpression
|
||||
val matchingClause: KtExpression?
|
||||
when (token) {
|
||||
KtTokens.EQEQ -> {
|
||||
throwExpression = thenClause.unwrapBlockOrParenthesis() as? KtThrowExpression ?: return null
|
||||
matchingClause = elseClause
|
||||
}
|
||||
|
||||
KtTokens.EXCLEQ -> {
|
||||
matchingClause = thenClause
|
||||
throwExpression = elseClause?.unwrapBlockOrParenthesis() as? KtThrowExpression ?: return null
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
val matchesAsStatement = element.isUsedAsStatement(element.analyze()) && (matchingClause?.isNullExpressionOrEmptyBlock() ?: true)
|
||||
if (!matchesAsStatement && !(matchingClause?.evaluatesTo(expression) ?: false && expression.isStableVariable())) return null
|
||||
val matchesAsStatement = element.isUsedAsStatement(context) && (baseClause?.isNullExpressionOrEmptyBlock() ?: true)
|
||||
if (!matchesAsStatement &&
|
||||
!(baseClause?.evaluatesTo(receiverExpression) ?: false && receiverExpression.isStableVariable())) return null
|
||||
|
||||
var text = "Replace 'if' expression with '!!' expression"
|
||||
if (!throwExpression.throwsNullPointerExceptionWithNoArguments()) {
|
||||
@@ -67,9 +50,8 @@ class IfThenToDoubleBangIntention : SelfTargetingRangeIntention<KtIfExpression>(
|
||||
}
|
||||
|
||||
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
||||
val condition = element.condition as KtBinaryExpression
|
||||
val expression = condition.expressionComparedToNull()!!
|
||||
val result = element.replace(KtPsiFactory(element).createExpressionByPattern("$0!!", expression)) as KtPostfixExpression
|
||||
val (_, _, receiverExpression) = element.buildSelectTransformationData() ?: return
|
||||
val result = element.replace(KtPsiFactory(element).createExpressionByPattern("$0!!", receiverExpression)) as KtPostfixExpression
|
||||
|
||||
result.inlineBaseExpressionIfApplicableWithPrompt(editor)
|
||||
}
|
||||
|
||||
+16
-102
@@ -24,17 +24,11 @@ import org.jetbrains.kotlin.idea.core.replaced
|
||||
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
|
||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingOffsetIndependentIntention
|
||||
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
||||
import org.jetbrains.kotlin.idea.intentions.getLeftMostReceiverExpression
|
||||
import org.jetbrains.kotlin.idea.intentions.replaceFirstReceiver
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
|
||||
|
||||
class IfThenToElvisInspection : IntentionBasedInspection<KtIfExpression>(
|
||||
IfThenToElvisIntention::class,
|
||||
@@ -46,49 +40,21 @@ class IfThenToElvisIntention : SelfTargetingOffsetIndependentIntention<KtIfExpre
|
||||
"Replace 'if' expression with elvis expression"
|
||||
) {
|
||||
|
||||
private fun KtExpression.clausesReplaceableByElvis(firstClause: KtExpression, secondClause: KtExpression, context: BindingContext) =
|
||||
!firstClause.isNullOrBlockExpression() &&
|
||||
(secondClause.evaluatesTo(this) || secondClause.hasFirstReceiverOf(this) && !secondClause.hasNullableType(context)) &&
|
||||
!(firstClause is KtThrowExpression && firstClause.throwsNullPointerExceptionWithNoArguments())
|
||||
|
||||
private fun KtExpression.checkedExpression() = when (this) {
|
||||
is KtBinaryExpression -> expressionComparedToNull()
|
||||
is KtIsExpression -> leftHandSide
|
||||
else -> null
|
||||
}
|
||||
private fun IfThenToSelectData.clausesReplaceableByElvis() =
|
||||
baseClause != null && negatedClause != null &&
|
||||
!negatedClause.isNullOrBlockExpression() &&
|
||||
(baseClause.evaluatesTo(receiverExpression) ||
|
||||
baseClause.hasFirstReceiverOf(receiverExpression) && !baseClause.hasNullableType(context)) &&
|
||||
!(negatedClause is KtThrowExpression && negatedClause.throwsNullPointerExceptionWithNoArguments())
|
||||
|
||||
override fun isApplicableTo(element: KtIfExpression): Boolean {
|
||||
val context = element.analyze()
|
||||
val type = element.getType(context) ?: return false
|
||||
val ifThenToSelectData = element.buildSelectTransformationData() ?: return false
|
||||
if (!ifThenToSelectData.receiverExpression.isStableVariable(ifThenToSelectData.context)) return false
|
||||
|
||||
val type = element.getType(ifThenToSelectData.context) ?: return false
|
||||
if (KotlinBuiltIns.isUnit(type)) return false
|
||||
|
||||
val condition = element.condition as? KtOperationExpression ?: return false
|
||||
val thenClause = element.then ?: return false
|
||||
val elseClause = element.`else` ?: return false
|
||||
|
||||
val checkedExpression = condition.checkedExpression() ?: return false
|
||||
if (!checkedExpression.isStableVariable(context)) return false
|
||||
|
||||
return when (condition) {
|
||||
is KtBinaryExpression -> when (condition.operationToken) {
|
||||
KtTokens.EQEQ -> checkedExpression.clausesReplaceableByElvis(thenClause, elseClause, context)
|
||||
KtTokens.EXCLEQ -> checkedExpression.clausesReplaceableByElvis(elseClause, thenClause, context)
|
||||
else -> false
|
||||
}
|
||||
is KtIsExpression -> {
|
||||
val targetType = context[BindingContext.TYPE, condition.typeReference] ?: return false
|
||||
if (TypeUtils.isNullableType(targetType)) return false
|
||||
// The following check can be removed after fix of KT-14576
|
||||
val originalType = condition.leftHandSide.getType(context) ?: return false
|
||||
if (!targetType.isSubtypeOf(originalType)) return false
|
||||
|
||||
when (condition.isNegated) {
|
||||
true -> checkedExpression.clausesReplaceableByElvis(thenClause, elseClause, context)
|
||||
false -> checkedExpression.clausesReplaceableByElvis(elseClause, thenClause, context)
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
return ifThenToSelectData.clausesReplaceableByElvis()
|
||||
}
|
||||
|
||||
private fun KtExpression.isNullOrBlockExpression(): Boolean {
|
||||
@@ -96,69 +62,17 @@ class IfThenToElvisIntention : SelfTargetingOffsetIndependentIntention<KtIfExpre
|
||||
return innerExpression is KtBlockExpression || innerExpression.node.elementType == KtNodeTypes.NULL
|
||||
}
|
||||
|
||||
private fun KtExpression.hasNullableType(context: BindingContext): Boolean {
|
||||
val type = getType(context) ?: return true
|
||||
return TypeUtils.isNullableType(type)
|
||||
}
|
||||
|
||||
private fun KtExpression.hasFirstReceiverOf(receiver: KtExpression): Boolean {
|
||||
val actualReceiver = (unwrapBlockOrParenthesis() as? KtDotQualifiedExpression)?.getLeftMostReceiverExpression() ?: return false
|
||||
return actualReceiver.evaluatesTo(receiver)
|
||||
}
|
||||
|
||||
private fun KtExpression.insertSafeCalls(factory: KtPsiFactory): KtExpression {
|
||||
if (this !is KtQualifiedExpression) return this
|
||||
if (this is KtDotQualifiedExpression) {
|
||||
operationTokenNode.psi.replace(factory.createSafeCallNode().psi)
|
||||
}
|
||||
receiverExpression.insertSafeCalls(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startInWriteAction() = false
|
||||
|
||||
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
||||
val condition = element.condition as KtOperationExpression
|
||||
|
||||
val thenClause = element.then!!
|
||||
val elseClause = element.`else`!!
|
||||
val thenExpression = thenClause.unwrapBlockOrParenthesis()
|
||||
val elseExpression = elseClause.unwrapBlockOrParenthesis()
|
||||
|
||||
val (left, right) = when (condition) {
|
||||
is KtBinaryExpression -> when (condition.operationToken) {
|
||||
KtTokens.EQEQ -> Pair(elseExpression, thenExpression)
|
||||
KtTokens.EXCLEQ -> Pair(thenExpression, elseExpression)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
is KtIsExpression -> when (condition.isNegated) {
|
||||
true -> Pair(elseExpression, thenExpression)
|
||||
false -> Pair(thenExpression, elseExpression)
|
||||
}
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
val ifThenToSelectData = element.buildSelectTransformationData() ?: return
|
||||
|
||||
val factory = KtPsiFactory(element)
|
||||
val newReceiver = (condition as? KtIsExpression)?.let {
|
||||
factory.createExpressionByPattern("$0 as? $1",
|
||||
(left as? KtDotQualifiedExpression)?.getLeftMostReceiverExpression() ?: left,
|
||||
it.typeReference!!)
|
||||
}
|
||||
val checkedExpression = condition.checkedExpression()!!
|
||||
val elvis = runWriteAction {
|
||||
val replacedLeft = if (left.evaluatesTo(checkedExpression)) {
|
||||
if (condition is KtIsExpression) newReceiver!! else left
|
||||
}
|
||||
else {
|
||||
if (condition is KtIsExpression) {
|
||||
(left as KtDotQualifiedExpression).replaceFirstReceiver(
|
||||
factory, newReceiver!!, safeAccess = true)
|
||||
}
|
||||
else {
|
||||
left.insertSafeCalls(factory)
|
||||
}
|
||||
}
|
||||
val newExpr = element.replaced(factory.createExpressionByPattern("$0 ?: $1", replacedLeft, right))
|
||||
val replacedBaseClause = ifThenToSelectData.replacedBaseClause(factory)
|
||||
val newExpr = element.replaced(factory.createExpressionByPattern("$0 ?: $1",
|
||||
replacedBaseClause,
|
||||
ifThenToSelectData.negatedClause!!))
|
||||
KtPsiUtil.deparenthesize(newExpr) as KtBinaryExpression
|
||||
}
|
||||
|
||||
|
||||
+16
-73
@@ -17,18 +17,12 @@
|
||||
package org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.core.replaced
|
||||
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
|
||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingOffsetIndependentIntention
|
||||
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
|
||||
|
||||
class IfThenToSafeAccessInspection : IntentionBasedInspection<KtIfExpression>(IfThenToSafeAccessIntention::class)
|
||||
|
||||
@@ -37,83 +31,32 @@ class IfThenToSafeAccessIntention : SelfTargetingOffsetIndependentIntention<KtIf
|
||||
) {
|
||||
|
||||
override fun isApplicableTo(element: KtIfExpression): Boolean {
|
||||
val condition = element.condition
|
||||
val thenClause = element.then
|
||||
val elseClause = element.`else`
|
||||
return when (condition) {
|
||||
is KtBinaryExpression -> {
|
||||
val receiverExpression = condition.expressionComparedToNull() ?: return false
|
||||
if (!receiverExpression.isStableVariable()) return false
|
||||
when (condition.operationToken) {
|
||||
KtTokens.EQEQ -> canBeReplacedWithSafeCall(receiverExpression, thenClause, elseClause)
|
||||
KtTokens.EXCLEQ -> canBeReplacedWithSafeCall(receiverExpression, elseClause, thenClause)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
is KtIsExpression -> {
|
||||
val context = element.analyze()
|
||||
val receiverExpression = condition.leftHandSide
|
||||
if (!receiverExpression.isStableVariable(context)) return false
|
||||
val targetType = context[BindingContext.TYPE, condition.typeReference] ?: return false
|
||||
if (TypeUtils.isNullableType(targetType)) return false
|
||||
val originalType = receiverExpression.getType(context) ?: return false
|
||||
if (!targetType.isSubtypeOf(originalType)) return false
|
||||
when(condition.isNegated) {
|
||||
true -> canBeReplacedWithSafeCall(receiverExpression, thenClause, elseClause)
|
||||
false -> canBeReplacedWithSafeCall(receiverExpression, elseClause, thenClause)
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
val ifThenToSelectData = element.buildSelectTransformationData() ?: return false
|
||||
if (!ifThenToSelectData.receiverExpression.isStableVariable(ifThenToSelectData.context)) return false
|
||||
|
||||
return ifThenToSelectData.clausesReplaceableBySafeCall()
|
||||
}
|
||||
|
||||
override fun startInWriteAction() = false
|
||||
|
||||
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
||||
val condition = element.condition
|
||||
val newExpr = when (condition) {
|
||||
is KtBinaryExpression -> {
|
||||
val receiverExpression = condition.expressionComparedToNull()!!
|
||||
val ifThenToSelectData = element.buildSelectTransformationData() ?: return
|
||||
|
||||
val selectorExpression =
|
||||
when(condition.operationToken) {
|
||||
KtTokens.EQEQ -> findSelectorExpressionInClause(element.`else`!!, receiverExpression)!!
|
||||
KtTokens.EXCLEQ -> findSelectorExpressionInClause(element.then!!, receiverExpression)!!
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
KtPsiFactory(element).createExpressionByPattern("$0?.$1", receiverExpression, selectorExpression) as KtSafeQualifiedExpression
|
||||
}
|
||||
is KtIsExpression -> {
|
||||
val typeRef = condition.typeReference!!
|
||||
val lhs = condition.leftHandSide
|
||||
val selector = if (condition.isNegated) element.`else`!! else element.then!!
|
||||
val selectorExpression = findSelectorExpressionInClause(selector, lhs)!!
|
||||
KtPsiFactory(element).createExpressionByPattern("($0 as? $1)?.$2", lhs, typeRef, selectorExpression) as KtSafeQualifiedExpression
|
||||
}
|
||||
else -> null
|
||||
val factory = KtPsiFactory(element)
|
||||
val resultExpr = runWriteAction {
|
||||
val replacedBaseClause = ifThenToSelectData.replacedBaseClause(factory)
|
||||
val newExpr = element.replaced(replacedBaseClause)
|
||||
KtPsiUtil.deparenthesize(newExpr)
|
||||
}
|
||||
|
||||
if (newExpr != null) {
|
||||
val safeAccessExpr = runWriteAction { element.replaced(newExpr) }
|
||||
if (editor != null) {
|
||||
safeAccessExpr.inlineReceiverIfApplicableWithPrompt(editor)
|
||||
}
|
||||
if (editor != null) {
|
||||
(resultExpr as? KtSafeQualifiedExpression)?.inlineReceiverIfApplicableWithPrompt(editor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canBeReplacedWithSafeCall(receiver: KtExpression, nullClause: KtExpression?, notNullClause: KtExpression?) =
|
||||
nullClause?.isNullExpression() ?: true &&
|
||||
notNullClause != null && clauseContainsAppropriateDotQualifiedExpression(notNullClause, receiver)
|
||||
|
||||
private fun clauseContainsAppropriateDotQualifiedExpression(clause: KtExpression, receiverExpression: KtExpression)
|
||||
= findSelectorExpressionInClause(clause, receiverExpression) != null
|
||||
|
||||
private fun findSelectorExpressionInClause(clause: KtExpression, receiverExpression: KtExpression): KtExpression? {
|
||||
val expression = clause.unwrapBlockOrParenthesis() as? KtDotQualifiedExpression ?: return null
|
||||
|
||||
if (expression.receiverExpression.text != receiverExpression.text) return null
|
||||
|
||||
return expression.selectorExpression
|
||||
private fun IfThenToSelectData.clausesReplaceableBySafeCall(): Boolean {
|
||||
if (baseClause == null || negatedClause != null && !negatedClause.isNullExpression()) return false
|
||||
return baseClause.evaluatesTo(receiverExpression) && condition !is KtBinaryExpression ||
|
||||
baseClause.hasFirstReceiverOf(receiverExpression) && !baseClause.hasNullableType(context)
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -111,4 +111,28 @@
|
||||
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">If-Then foldable to '?.'</problem_class>
|
||||
<description>Replace 'if' expression with safe access expression</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>nullCheckWithSelectorCallChain.kt</file>
|
||||
<line>5</line>
|
||||
<module>light_idea_test_case</module>
|
||||
<entry_point TYPE="file" FQNAME="temp:///src/nullCheckWithSelectorCallChain.kt" />
|
||||
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">If-Then foldable to '?.'</problem_class>
|
||||
<description>Replace 'if' expression with safe access expression</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>isCheckWithSelectorChain.kt</file>
|
||||
<line>4</line>
|
||||
<module>light_idea_test_case</module>
|
||||
<entry_point TYPE="file" FQNAME="temp:///src/isCheckWithSelectorChain.kt" />
|
||||
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">If-Then foldable to '?.'</problem_class>
|
||||
<description>Replace 'if' expression with safe access expression</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>isCheckSimple.kt</file>
|
||||
<line>4</line>
|
||||
<module>light_idea_test_case</module>
|
||||
<entry_point TYPE="file" FQNAME="temp:///src/isCheckSimple.kt" />
|
||||
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">If-Then foldable to '?.'</problem_class>
|
||||
<description>Replace 'if' expression with safe access expression</description>
|
||||
</problem>
|
||||
</problems>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class My(val x: Int)
|
||||
|
||||
fun foo(arg: Any?): My? {
|
||||
return if (<caret>arg is My) arg else null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
class My(val x: Int)
|
||||
|
||||
fun foo(arg: Any?): My? {
|
||||
return arg as? My
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
class My(val x: Int)
|
||||
|
||||
fun foo(arg: Any?): Int? {
|
||||
return if (<caret>arg is My) arg.x.hashCode() else null
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
class My(val x: Int)
|
||||
|
||||
fun foo(arg: Any?): Int? {
|
||||
return (arg as? My)?.x?.hashCode()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// IS_APPLICABLE: false
|
||||
|
||||
fun foo(arg: Any?): Any? {
|
||||
return if (<caret>arg != null) arg else null
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// WITH_RUNTIME
|
||||
|
||||
val nullableString: String? = "abc"
|
||||
|
||||
val foo = if (<caret>nullableString != null) {
|
||||
nullableString.toUpperCase().toLowerCase()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// WITH_RUNTIME
|
||||
|
||||
val nullableString: String? = "abc"
|
||||
|
||||
val foo = nullableString?.toUpperCase()?.toLowerCase()
|
||||
@@ -1766,6 +1766,18 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("isCheckSimple.kt")
|
||||
public void testIsCheckSimple() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/isCheckSimple.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("isCheckWithSelectorChain.kt")
|
||||
public void testIsCheckWithSelectorChain() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/isCheckWithSelectorChain.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("isCondition.kt")
|
||||
public void testIsCondition() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/isCondition.kt");
|
||||
@@ -1856,6 +1868,18 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("nullCheckSimple.kt")
|
||||
public void testNullCheckSimple() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/nullCheckSimple.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("nullCheckWithSelectorCallChain.kt")
|
||||
public void testNullCheckWithSelectorCallChain() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/nullCheckWithSelectorCallChain.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("otherBlockHasMoreThanOneStatement.kt")
|
||||
public void testOtherBlockHasMoreThanOneStatement() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/otherBlockHasMoreThanOneStatement.kt");
|
||||
|
||||
Reference in New Issue
Block a user