Convert to scope function: refactor

This commit is contained in:
Toshiaki Kameyama
2018-12-18 16:25:17 +03:00
committed by Mikhail Glukhikh
parent 0501a108c0
commit f15c8f78fa
108 changed files with 592 additions and 541 deletions
@@ -1,46 +0,0 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiClass
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
abstract class ConvertDotQualifiedToScopeIntention(
text: String
) : ConvertToScopeIntention<KtDotQualifiedExpression>(KtDotQualifiedExpression::class.java, text) {
override val isParameterScopeFunction = false
override fun isApplicableTo(element: KtDotQualifiedExpression, caretOffset: Int): Boolean {
val receiverExpression = element.getLeftMostReceiverExpression()
if (receiverExpression.mainReference?.resolve() is PsiClass) return false
val receiverExpressionText = receiverExpression.text
if (receiverExpressionText == scopeReceiverName) return false
if (!isApplicableWithGivenReceiverText(element, receiverExpressionText)) return false
val nextSibling = element.getDotQualifiedSiblingIfAny(forward = true)
if (nextSibling != null && isApplicableWithGivenReceiverText(nextSibling, receiverExpressionText)) return true
val prevSibling = element.getDotQualifiedSiblingIfAny(forward = false)
return prevSibling != null && isApplicableWithGivenReceiverText(prevSibling, receiverExpressionText)
}
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
val receiverExpressionText = element.getReceiverExpressionText() ?: return
applyWithGivenReceiverText(element, receiverExpressionText)
}
}
@@ -1,95 +0,0 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
sealed class ConvertToApplyOrAlsoIntention(isAlso: Boolean) : ConvertToScopeIntention<KtExpression>(
KtExpression::class.java, "Convert to ${scopeFunctionName(isAlso)}"
) {
companion object {
fun scopeFunctionName(isParameterScopeFunction: Boolean) = if (isParameterScopeFunction) "also" else "apply"
}
override val isParameterScopeFunction = isAlso
override fun findCallExpressionFrom(scopeExpression: KtExpression) =
((scopeExpression as? KtProperty)?.initializer as? KtQualifiedExpression)?.callExpression
override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean {
return when (element) {
is KtProperty -> element.isApplicable()
is KtDotQualifiedExpression -> {
val receiverExpressionText = element.getLeftMostReceiverExpression().text
isApplicableWithGivenReceiverText(element, receiverExpressionText) &&
element.findTargetProperty(receiverExpressionText)?.isApplicable() ?: false
}
else -> false
}
}
private fun KtProperty.isApplicable(): Boolean {
if (!isLocal) return false
val localVariableName = name ?: return false
val firstDotQualified = getDotQualifiedSiblingIfAny(forward = true)
if (firstDotQualified != null && isApplicableWithGivenReceiverText(firstDotQualified, localVariableName)) {
val nextDotQualified = firstDotQualified.getDotQualifiedSiblingIfAny(forward = true)
return nextDotQualified != null && isApplicableWithGivenReceiverText(nextDotQualified, localVariableName)
}
return false
}
private fun KtDotQualifiedExpression.findTargetProperty(receiverExpressionText: String): KtProperty? {
val target = getPrevSiblingIgnoringWhitespaceAndComments(false)
when (target) {
is KtProperty ->
if (target.name == receiverExpressionText) {
return target
}
is KtDotQualifiedExpression ->
if (isApplicableWithGivenReceiverText(target, receiverExpressionText)) {
return target.findTargetProperty(receiverExpressionText)
}
}
return null
}
override fun applyTo(element: KtExpression, editor: Editor?) {
when (element) {
is KtProperty -> applyWithGivenReceiverText(element, element.name ?: return)
is KtDotQualifiedExpression -> {
val receiverExpressionText = element.getReceiverExpressionText() ?: return
val property = element.findTargetProperty(receiverExpressionText) ?: return
applyWithGivenReceiverText(property, receiverExpressionText)
}
}
}
override fun createScopeExpression(factory: KtPsiFactory, element: KtExpression): KtProperty? {
if (element !is KtProperty) return null
val receiverExpressionText = element.name ?: return null
return factory.createProperty(receiverExpressionText, element.typeReference?.text, element.isVar,
"${element.initializer?.text}.${scopeFunctionName(isParameterScopeFunction)}{}")
}
}
class ConvertToApplyIntention : ConvertToApplyOrAlsoIntention(isAlso = false)
class ConvertToAlsoIntention : ConvertToApplyOrAlsoIntention(isAlso = true)
@@ -1,28 +0,0 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions
import org.jetbrains.kotlin.psi.*
class ConvertToRunIntention : ConvertDotQualifiedToScopeIntention("Convert to run") {
override fun createScopeExpression(factory: KtPsiFactory, element: KtDotQualifiedExpression) =
factory.createExpressionByPattern("$0.run {}", element.getLeftMostReceiverExpression())
override fun findCallExpressionFrom(scopeExpression: KtExpression) =
(scopeExpression as? KtQualifiedExpression)?.callExpression
}
@@ -16,92 +16,150 @@
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.idea.intentions.ConvertToScopeIntention.ScopeFunction.*
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.*
abstract class ConvertToScopeIntention<TExpression : KtExpression>(
elementType: Class<TExpression>,
text: String
) : SelfTargetingIntention<TExpression>(elementType, text) {
sealed class ConvertToScopeIntention(
private val scopeFunction: ScopeFunction
) : SelfTargetingIntention<KtExpression>(KtExpression::class.java, "Convert to ${scopeFunction.functionName}") {
abstract val isParameterScopeFunction: Boolean
enum class ScopeFunction(val functionName: String, val isParameterScope: Boolean) {
ALSO(functionName = "also", isParameterScope = true),
APPLY(functionName = "apply", isParameterScope = false),
RUN(functionName = "run", isParameterScope = false),
WITH(functionName = "with", isParameterScope = false);
protected val scopeReceiverName: String
get() = if (isParameterScopeFunction) "it" else "this"
protected abstract fun createScopeExpression(factory: KtPsiFactory, element: TExpression): KtExpression?
protected abstract fun findCallExpressionFrom(scopeExpression: KtExpression): KtCallExpression?
protected fun KtDotQualifiedExpression.getReceiverExpressionText(): String? = getLeftMostReceiverExpression().text
protected fun isApplicableWithGivenReceiverText(expression: KtDotQualifiedExpression, receiverExpressionText: String): Boolean {
if (receiverExpressionText != expression.getReceiverExpressionText()) return false
val callExpression = expression.callExpression ?: return false
if (!callExpression.isApplicable()) return false
val receiverExpression = expression.receiverExpression
return receiverExpression !is KtDotQualifiedExpression ||
isApplicableWithGivenReceiverText(receiverExpression, receiverExpressionText)
val receiver = if (isParameterScope) "it" else "this"
}
protected fun applyWithGivenReceiverText(expression: TExpression, receiverExpressionText: String) {
val factory = KtPsiFactory(expression)
val scopeBlockExpression = createScopeExpression(factory, expression) ?: return
val callExpression = findCallExpressionFrom(scopeBlockExpression) ?: return
val blockExpression = callExpression.getFirstLambdaArgumentBody() ?: return
val parent = expression.parent
val lastExpressionToMove = findLastExpressionToMove(receiverExpressionText, expression)
val firstTargetExpression =
if (expression is KtProperty) expression
else findFirstExpressionToMove(receiverExpressionText, expression)
val firstExpressionToMove = if (expression is KtProperty) expression.nextSibling else firstTargetExpression
blockExpression.moveRangeInto(firstExpressionToMove, lastExpressionToMove, factory)
parent.addBefore(scopeBlockExpression, firstTargetExpression)
parent.deleteChildRange(firstTargetExpression, lastExpressionToMove)
}
protected fun KtExpression.getDotQualifiedSiblingIfAny(forward: Boolean): KtDotQualifiedExpression? {
val sibling =
if (forward) getNextSiblingIgnoringWhitespaceAndComments(false)
else getPrevSiblingIgnoringWhitespaceAndComments(false)
return sibling as? KtDotQualifiedExpression
}
private fun KtCallExpression.getFirstLambdaArgumentBody() =
lambdaArguments.firstOrNull()?.getLambdaExpression()?.bodyExpression
private fun KtBlockExpression.moveRangeInto(
firstElement: PsiElement, lastElement: PsiElement, psiFactory: KtPsiFactory
) {
addRange(firstElement, lastElement)
children.filterIsInstance(KtDotQualifiedExpression::class.java)
.forEach {
val replaced = it.deleteFirstReceiver()
if (isParameterScopeFunction) {
replaced.replace(psiFactory.createExpressionByPattern("$scopeReceiverName.$0", replaced))
}
}
}
private fun KtCallExpression.isApplicable() = lambdaArguments.isEmpty() && valueArguments.all { it.text != scopeReceiverName }
private fun findFirstExpressionToMove(receiverExpressionText: String, expression: KtExpression) =
findBoundaryExpression(receiverExpressionText, expression, forward = false)
private fun findLastExpressionToMove(receiverExpressionText: String, expression: KtExpression) =
findBoundaryExpression(receiverExpressionText, expression, forward = true)
private fun findBoundaryExpression(receiverExpressionText: String, expression: KtExpression, forward: Boolean): KtExpression {
var targetExpression: KtExpression = expression
while (true) {
val dotQualifiedSibling = targetExpression.getDotQualifiedSiblingIfAny(forward)
if (dotQualifiedSibling == null || !isApplicableWithGivenReceiverText(dotQualifiedSibling, receiverExpressionText)) {
return targetExpression
override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean {
when (element) {
is KtProperty ->
if (!element.isLocal) return false
is KtDotQualifiedExpression -> {
if (element.parent is KtDotQualifiedExpression) return false
val name = element.getLeftMostReceiverExpression().text
if (!element.isTarget(name)) return false
}
targetExpression = dotQualifiedSibling
else ->
return false
}
return element.collectTargetElements() != null
}
}
override fun applyTo(element: KtExpression, editor: Editor?) {
val targets = element.collectTargetElements() ?: return
val first = targets.firstOrNull() ?: return
val last = targets.lastOrNull() ?: return
val property = element.prevProperty()
val propertyOrFirst = when (scopeFunction) {
ALSO, APPLY -> property
else -> first
} ?: return
val parent = element.parent
val psiFactory = KtPsiFactory(element)
val (scopeFunctionCall, block) = psiFactory.createScopeFunctionCall(propertyOrFirst) ?: return
block.addRange(property?.nextSibling ?: first, last)
block.children.filterIsInstance(KtDotQualifiedExpression::class.java)
.forEach {
val replaced = it.deleteFirstReceiver()
if (scopeFunction.isParameterScope) {
replaced.replace(psiFactory.createExpressionByPattern("${scopeFunction.receiver}.$0", replaced))
}
}
parent.addBefore(scopeFunctionCall, propertyOrFirst)
parent.deleteChildRange(propertyOrFirst, last)
}
private fun KtExpression.collectTargetElements(): List<PsiElement>? {
val targets = when (scopeFunction) {
ALSO, APPLY -> {
val property = prevProperty() ?: return null
val referenceName = property.name ?: return null
property.collectTargetElements(referenceName, forward = true).toList().takeIf { this is KtProperty || this in it }
}
else -> {
if (this !is KtDotQualifiedExpression) return null
val referenceName = getLeftMostReceiverExpression().text
val prev = collectTargetElements(referenceName, forward = false).toList().reversed()
val next = collectTargetElements(referenceName, forward = true)
prev + listOf(this) + next
}
}
return targets?.takeIf { it.size >= 2 }
}
private fun KtExpression.collectTargetElements(referenceName: String, forward: Boolean): Sequence<PsiElement> {
return siblings(forward, withItself = false)
.filter { it !is PsiWhiteSpace && it !is PsiComment }
.takeWhile { it.isTarget(referenceName) }
}
private fun PsiElement.isTarget(referenceName: String): Boolean {
if (this !is KtDotQualifiedExpression) return false
val leftMostReceiver = getLeftMostReceiverExpression()
if (leftMostReceiver.text != referenceName) return false
if (leftMostReceiver.mainReference?.resolve() is PsiClass) return false
val callExpr = callExpression ?: return false
if (callExpr.lambdaArguments.isNotEmpty() || callExpr.valueArguments.any { it.text == scopeFunction.receiver }) return false
return !anyDescendantOfType<KtNameReferenceExpression> { it.text == scopeFunction.receiver }
}
private fun KtExpression.prevProperty(): KtProperty? {
return siblings(forward = false, withItself = true).firstOrNull { it is KtProperty && it.isLocal } as? KtProperty
}
private fun KtPsiFactory.createScopeFunctionCall(element: PsiElement): Pair<KtExpression, KtBlockExpression>? {
val scopeFunctionName = scopeFunction.functionName
val (scopeFunctionCall, callExpression) = when (scopeFunction) {
ALSO, APPLY -> {
if (element !is KtProperty) return null
val propertyName = element.name ?: return null
val initializer = element.initializer ?: return null
val property = createProperty(
name = propertyName,
type = element.typeReference?.text,
isVar = element.isVar,
initializer = "${initializer.text}.$scopeFunctionName {}"
)
val callExpression = (property.initializer as? KtDotQualifiedExpression)?.callExpression ?: return null
property to callExpression
}
RUN -> {
if (element !is KtDotQualifiedExpression) return null
val scopeFunctionCall = createExpressionByPattern(
"$0.$scopeFunctionName {}",
element.getLeftMostReceiverExpression()
) as? KtQualifiedExpression ?: return null
val callExpression = scopeFunctionCall.callExpression ?: return null
scopeFunctionCall to callExpression
}
WITH -> {
if (element !is KtDotQualifiedExpression) return null
val scopeFunctionCall = createExpressionByPattern(
"$scopeFunctionName($0) {}",
element.getLeftMostReceiverExpression()
) as? KtCallExpression ?: return null
scopeFunctionCall to scopeFunctionCall
}
}
val body = callExpression.lambdaArguments.firstOrNull()?.getLambdaExpression()?.bodyExpression ?: return null
return scopeFunctionCall to body
}
}
class ConvertToAlsoIntention : ConvertToScopeIntention(ALSO)
class ConvertToApplyIntention : ConvertToScopeIntention(APPLY)
class ConvertToRunIntention : ConvertToScopeIntention(RUN)
class ConvertToWithIntention : ConvertToScopeIntention(WITH)
@@ -1,27 +0,0 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions
import org.jetbrains.kotlin.psi.*
class ConvertToWithIntention : ConvertDotQualifiedToScopeIntention("Convert to with") {
override fun createScopeExpression(factory: KtPsiFactory, element: KtDotQualifiedExpression) =
factory.createExpressionByPattern("with($0) {}", element.getLeftMostReceiverExpression())
override fun findCallExpressionFrom(scopeExpression: KtExpression) = scopeExpression as? KtCallExpression
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
<caret>val a = MyClass()
a.foo1()
a.foo2()
a.foo3()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
val a = MyClass().also {
it.foo1()
it.foo2()
it.foo3()
}
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
<caret>val a = MyClass()
a.foo1()
a.foo2()
a.foo3()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
val a = MyClass().apply {
foo1()
foo2()
foo3()
}
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
val a = MyClass()
a.foo1()<caret>
a.foo2()
a.foo3()
}
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
val a = MyClass()
a.run {
foo1()
foo2()
foo3()
}
}
}
@@ -0,0 +1,14 @@
// IS_APPLICABLE: false
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
<caret>val a = MyClass()
a.foo1()
a.foo2()
a.foo3()
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
val a = MyClass()
a.foo1()<caret>
a.foo2()
a.foo3()
}
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
val a = MyClass()
with(a) {
foo1()
foo2()
foo3()
}
}
}

Some files were not shown because too many files have changed in this diff Show More