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.CallableDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
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.core.setType
|
||||||
import org.jetbrains.kotlin.idea.references.mainReference
|
import org.jetbrains.kotlin.idea.references.mainReference
|
||||||
import org.jetbrains.kotlin.lexer.KtTokens
|
import org.jetbrains.kotlin.lexer.KtTokens
|
||||||
@@ -248,19 +249,19 @@ fun KtDotQualifiedExpression.replaceFirstReceiver(
|
|||||||
newReceiver: KtExpression,
|
newReceiver: KtExpression,
|
||||||
safeAccess: Boolean = false
|
safeAccess: Boolean = false
|
||||||
): KtExpression {
|
): KtExpression {
|
||||||
val receiver = receiverExpression
|
val replaced = (if (safeAccess) {
|
||||||
if (safeAccess) {
|
this.replaced(factory.createExpressionByPattern("$0?.$1", receiverExpression, selectorExpression!!))
|
||||||
operationTokenNode.psi.replace(factory.createSafeCallNode().psi)
|
} else this) as KtQualifiedExpression
|
||||||
}
|
val receiver = replaced.receiverExpression
|
||||||
when (receiver) {
|
when (receiver) {
|
||||||
is KtDotQualifiedExpression -> {
|
is KtDotQualifiedExpression -> {
|
||||||
receiver.replaceFirstReceiver(factory, newReceiver, safeAccess)
|
receiver.replace(receiver.replaceFirstReceiver(factory, newReceiver, safeAccess))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
receiver.replace(newReceiver)
|
receiver.replace(newReceiver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this
|
return replaced
|
||||||
}
|
}
|
||||||
|
|
||||||
fun KtDotQualifiedExpression.deleteFirstReceiver(): KtExpression {
|
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.descriptors.VariableDescriptor
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||||
import org.jetbrains.kotlin.idea.core.replaced
|
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.inline.KotlinInlineValHandler
|
||||||
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler
|
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler
|
||||||
import org.jetbrains.kotlin.idea.references.mainReference
|
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.BindingContext
|
||||||
import org.jetbrains.kotlin.resolve.BindingContextUtils
|
import org.jetbrains.kotlin.resolve.BindingContextUtils
|
||||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
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.calls.smartcasts.DataFlowValueFactory
|
||||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
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
|
import org.jetbrains.kotlin.utils.addToStdlib.constant
|
||||||
|
|
||||||
fun KtBinaryExpression.expressionComparedToNull(): KtExpression? {
|
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 KtExpression?.isNullExpressionOrEmptyBlock(): Boolean = this.isNullExpression() || this is KtBlockExpression && this.statements.isEmpty()
|
||||||
|
|
||||||
fun KtThrowExpression.throwsNullPointerExceptionWithNoArguments(): Boolean {
|
fun KtThrowExpression.throwsNullPointerExceptionWithNoArguments(): Boolean {
|
||||||
val thrownExpression = this.thrownExpression
|
val thrownExpression = this.thrownExpression as? KtCallExpression ?: return false
|
||||||
if (thrownExpression !is KtCallExpression) return false
|
|
||||||
|
|
||||||
val context = this.analyze(BodyResolveMode.PARTIAL)
|
val context = this.analyze(BodyResolveMode.PARTIAL)
|
||||||
val nameExpression = thrownExpression.calleeExpression as? KtNameReferenceExpression ?: return false
|
val nameExpression = thrownExpression.calleeExpression as? KtNameReferenceExpression ?: return false
|
||||||
@@ -135,3 +139,89 @@ fun KtExpression.isStableVariable(context: BindingContext = this.analyze()): Boo
|
|||||||
return descriptor is VariableDescriptor &&
|
return descriptor is VariableDescriptor &&
|
||||||
DataFlowValueFactory.isStableValue(descriptor, DescriptorUtils.getContainingModule(descriptor))
|
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.editor.Editor
|
||||||
import com.intellij.openapi.util.TextRange
|
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.SelfTargetingRangeIntention
|
||||||
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
||||||
import org.jetbrains.kotlin.lexer.KtTokens
|
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsStatement
|
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? {
|
override fun applicabilityRange(element: KtIfExpression): TextRange? {
|
||||||
val condition = element.condition as? KtBinaryExpression ?: return null
|
val (context, condition, receiverExpression, baseClause, negatedClause) = element.buildSelectTransformationData() ?: return null
|
||||||
val thenClause = element.then ?: return null
|
// TODO: here "Replace with as" can be supported
|
||||||
val elseClause = element.`else`
|
if (condition is KtIsExpression) return null
|
||||||
|
|
||||||
val expression = condition.expressionComparedToNull() ?: return null
|
val throwExpression = negatedClause as? KtThrowExpression ?: return null
|
||||||
|
|
||||||
val token = condition.operationToken
|
val matchesAsStatement = element.isUsedAsStatement(context) && (baseClause?.isNullExpressionOrEmptyBlock() ?: true)
|
||||||
|
if (!matchesAsStatement &&
|
||||||
val throwExpression: KtThrowExpression
|
!(baseClause?.evaluatesTo(receiverExpression) ?: false && receiverExpression.isStableVariable())) return null
|
||||||
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
|
|
||||||
|
|
||||||
var text = "Replace 'if' expression with '!!' expression"
|
var text = "Replace 'if' expression with '!!' expression"
|
||||||
if (!throwExpression.throwsNullPointerExceptionWithNoArguments()) {
|
if (!throwExpression.throwsNullPointerExceptionWithNoArguments()) {
|
||||||
@@ -67,9 +50,8 @@ class IfThenToDoubleBangIntention : SelfTargetingRangeIntention<KtIfExpression>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
||||||
val condition = element.condition as KtBinaryExpression
|
val (_, _, receiverExpression) = element.buildSelectTransformationData() ?: return
|
||||||
val expression = condition.expressionComparedToNull()!!
|
val result = element.replace(KtPsiFactory(element).createExpressionByPattern("$0!!", receiverExpression)) as KtPostfixExpression
|
||||||
val result = element.replace(KtPsiFactory(element).createExpressionByPattern("$0!!", expression)) as KtPostfixExpression
|
|
||||||
|
|
||||||
result.inlineBaseExpressionIfApplicableWithPrompt(editor)
|
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.inspections.IntentionBasedInspection
|
||||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingOffsetIndependentIntention
|
import org.jetbrains.kotlin.idea.intentions.SelfTargetingOffsetIndependentIntention
|
||||||
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
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.idea.util.application.runWriteAction
|
||||||
import org.jetbrains.kotlin.lexer.KtTokens
|
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
import org.jetbrains.kotlin.resolve.BindingContext
|
|
||||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
|
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
|
||||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
|
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
|
||||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||||
import org.jetbrains.kotlin.types.TypeUtils
|
|
||||||
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
|
|
||||||
|
|
||||||
class IfThenToElvisInspection : IntentionBasedInspection<KtIfExpression>(
|
class IfThenToElvisInspection : IntentionBasedInspection<KtIfExpression>(
|
||||||
IfThenToElvisIntention::class,
|
IfThenToElvisIntention::class,
|
||||||
@@ -46,49 +40,21 @@ class IfThenToElvisIntention : SelfTargetingOffsetIndependentIntention<KtIfExpre
|
|||||||
"Replace 'if' expression with elvis expression"
|
"Replace 'if' expression with elvis expression"
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private fun KtExpression.clausesReplaceableByElvis(firstClause: KtExpression, secondClause: KtExpression, context: BindingContext) =
|
private fun IfThenToSelectData.clausesReplaceableByElvis() =
|
||||||
!firstClause.isNullOrBlockExpression() &&
|
baseClause != null && negatedClause != null &&
|
||||||
(secondClause.evaluatesTo(this) || secondClause.hasFirstReceiverOf(this) && !secondClause.hasNullableType(context)) &&
|
!negatedClause.isNullOrBlockExpression() &&
|
||||||
!(firstClause is KtThrowExpression && firstClause.throwsNullPointerExceptionWithNoArguments())
|
(baseClause.evaluatesTo(receiverExpression) ||
|
||||||
|
baseClause.hasFirstReceiverOf(receiverExpression) && !baseClause.hasNullableType(context)) &&
|
||||||
private fun KtExpression.checkedExpression() = when (this) {
|
!(negatedClause is KtThrowExpression && negatedClause.throwsNullPointerExceptionWithNoArguments())
|
||||||
is KtBinaryExpression -> expressionComparedToNull()
|
|
||||||
is KtIsExpression -> leftHandSide
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isApplicableTo(element: KtIfExpression): Boolean {
|
override fun isApplicableTo(element: KtIfExpression): Boolean {
|
||||||
val context = element.analyze()
|
val ifThenToSelectData = element.buildSelectTransformationData() ?: return false
|
||||||
val type = element.getType(context) ?: return false
|
if (!ifThenToSelectData.receiverExpression.isStableVariable(ifThenToSelectData.context)) return false
|
||||||
|
|
||||||
|
val type = element.getType(ifThenToSelectData.context) ?: return false
|
||||||
if (KotlinBuiltIns.isUnit(type)) return false
|
if (KotlinBuiltIns.isUnit(type)) return false
|
||||||
|
|
||||||
val condition = element.condition as? KtOperationExpression ?: return false
|
return ifThenToSelectData.clausesReplaceableByElvis()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KtExpression.isNullOrBlockExpression(): Boolean {
|
private fun KtExpression.isNullOrBlockExpression(): Boolean {
|
||||||
@@ -96,69 +62,17 @@ class IfThenToElvisIntention : SelfTargetingOffsetIndependentIntention<KtIfExpre
|
|||||||
return innerExpression is KtBlockExpression || innerExpression.node.elementType == KtNodeTypes.NULL
|
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 startInWriteAction() = false
|
||||||
|
|
||||||
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
||||||
val condition = element.condition as KtOperationExpression
|
val ifThenToSelectData = element.buildSelectTransformationData() ?: return
|
||||||
|
|
||||||
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 factory = KtPsiFactory(element)
|
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 elvis = runWriteAction {
|
||||||
val replacedLeft = if (left.evaluatesTo(checkedExpression)) {
|
val replacedBaseClause = ifThenToSelectData.replacedBaseClause(factory)
|
||||||
if (condition is KtIsExpression) newReceiver!! else left
|
val newExpr = element.replaced(factory.createExpressionByPattern("$0 ?: $1",
|
||||||
}
|
replacedBaseClause,
|
||||||
else {
|
ifThenToSelectData.negatedClause!!))
|
||||||
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))
|
|
||||||
KtPsiUtil.deparenthesize(newExpr) as KtBinaryExpression
|
KtPsiUtil.deparenthesize(newExpr) as KtBinaryExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-73
@@ -17,18 +17,12 @@
|
|||||||
package org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions
|
package org.jetbrains.kotlin.idea.intentions.branchedTransformations.intentions
|
||||||
|
|
||||||
import com.intellij.openapi.editor.Editor
|
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.core.replaced
|
||||||
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
|
import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
|
||||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingOffsetIndependentIntention
|
import org.jetbrains.kotlin.idea.intentions.SelfTargetingOffsetIndependentIntention
|
||||||
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.*
|
||||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||||
import org.jetbrains.kotlin.lexer.KtTokens
|
|
||||||
import org.jetbrains.kotlin.psi.*
|
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)
|
class IfThenToSafeAccessInspection : IntentionBasedInspection<KtIfExpression>(IfThenToSafeAccessIntention::class)
|
||||||
|
|
||||||
@@ -37,83 +31,32 @@ class IfThenToSafeAccessIntention : SelfTargetingOffsetIndependentIntention<KtIf
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
override fun isApplicableTo(element: KtIfExpression): Boolean {
|
override fun isApplicableTo(element: KtIfExpression): Boolean {
|
||||||
val condition = element.condition
|
val ifThenToSelectData = element.buildSelectTransformationData() ?: return false
|
||||||
val thenClause = element.then
|
if (!ifThenToSelectData.receiverExpression.isStableVariable(ifThenToSelectData.context)) return false
|
||||||
val elseClause = element.`else`
|
|
||||||
return when (condition) {
|
return ifThenToSelectData.clausesReplaceableBySafeCall()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startInWriteAction() = false
|
override fun startInWriteAction() = false
|
||||||
|
|
||||||
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
override fun applyTo(element: KtIfExpression, editor: Editor?) {
|
||||||
val condition = element.condition
|
val ifThenToSelectData = element.buildSelectTransformationData() ?: return
|
||||||
val newExpr = when (condition) {
|
|
||||||
is KtBinaryExpression -> {
|
|
||||||
val receiverExpression = condition.expressionComparedToNull()!!
|
|
||||||
|
|
||||||
val selectorExpression =
|
val factory = KtPsiFactory(element)
|
||||||
when(condition.operationToken) {
|
val resultExpr = runWriteAction {
|
||||||
KtTokens.EQEQ -> findSelectorExpressionInClause(element.`else`!!, receiverExpression)!!
|
val replacedBaseClause = ifThenToSelectData.replacedBaseClause(factory)
|
||||||
KtTokens.EXCLEQ -> findSelectorExpressionInClause(element.then!!, receiverExpression)!!
|
val newExpr = element.replaced(replacedBaseClause)
|
||||||
else -> throw IllegalArgumentException()
|
KtPsiUtil.deparenthesize(newExpr)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newExpr != null) {
|
if (editor != null) {
|
||||||
val safeAccessExpr = runWriteAction { element.replaced(newExpr) }
|
(resultExpr as? KtSafeQualifiedExpression)?.inlineReceiverIfApplicableWithPrompt(editor)
|
||||||
if (editor != null) {
|
|
||||||
safeAccessExpr.inlineReceiverIfApplicableWithPrompt(editor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canBeReplacedWithSafeCall(receiver: KtExpression, nullClause: KtExpression?, notNullClause: KtExpression?) =
|
private fun IfThenToSelectData.clausesReplaceableBySafeCall(): Boolean {
|
||||||
nullClause?.isNullExpression() ?: true &&
|
if (baseClause == null || negatedClause != null && !negatedClause.isNullExpression()) return false
|
||||||
notNullClause != null && clauseContainsAppropriateDotQualifiedExpression(notNullClause, receiver)
|
return baseClause.evaluatesTo(receiverExpression) && condition !is KtBinaryExpression ||
|
||||||
|
baseClause.hasFirstReceiverOf(receiverExpression) && !baseClause.hasNullableType(context)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -111,4 +111,28 @@
|
|||||||
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">If-Then foldable to '?.'</problem_class>
|
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">If-Then foldable to '?.'</problem_class>
|
||||||
<description>Replace 'if' expression with safe access expression</description>
|
<description>Replace 'if' expression with safe access expression</description>
|
||||||
</problem>
|
</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>
|
</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);
|
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")
|
@TestMetadata("isCondition.kt")
|
||||||
public void testIsCondition() throws Exception {
|
public void testIsCondition() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/isCondition.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/isCondition.kt");
|
||||||
@@ -1856,6 +1868,18 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
|
|||||||
doTest(fileName);
|
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")
|
@TestMetadata("otherBlockHasMoreThanOneStatement.kt")
|
||||||
public void testOtherBlockHasMoreThanOneStatement() throws Exception {
|
public void testOtherBlockHasMoreThanOneStatement() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/otherBlockHasMoreThanOneStatement.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/branched/ifThenToSafeAccess/otherBlockHasMoreThanOneStatement.kt");
|
||||||
|
|||||||
Reference in New Issue
Block a user