Add "Convert to also" intention

#KT-28699 Fixed
This commit is contained in:
Toshiaki Kameyama
2018-12-14 19:10:20 +09:00
parent c1f75d2324
commit 6a4c6eacd7
38 changed files with 503 additions and 27 deletions
@@ -1496,6 +1496,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ConvertToAlsoIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ConvertToWithIntention</className>
<category>Kotlin</category>
@@ -0,0 +1,7 @@
fun foo() {
<spot>val a = MyClass().also {
it.setFoo(1)
it.setBar(2)
it.setBaz(3)
}</spot>
}
@@ -0,0 +1,6 @@
fun foo() {
<spot>val a = MyClass()
a.setFoo(1)
a.setBar(2)
a.setBaz(3)</spot>
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention converts several calls with the same receiver to <b>also</b>.
</body>
</html>
@@ -25,11 +25,13 @@ 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 in BLACKLIST_RECEIVER_NAME) return false
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
@@ -20,9 +20,16 @@ import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
class ConvertToApplyIntention : ConvertToScopeIntention<KtExpression>(
KtExpression::class.java, "Convert to apply"
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
@@ -79,6 +86,10 @@ class ConvertToApplyIntention : ConvertToScopeIntention<KtExpression>(
if (element !is KtProperty) return null
val receiverExpressionText = element.name ?: return null
return factory.createProperty(receiverExpressionText, element.typeReference?.text, element.isVar,
"${element.initializer?.text}.apply{}")
"${element.initializer?.text}.${scopeFunctionName(isParameterScopeFunction)}{}")
}
}
}
class ConvertToApplyIntention : ConvertToApplyOrAlsoIntention(isAlso = false)
class ConvertToAlsoIntention : ConvertToApplyOrAlsoIntention(isAlso = true)
@@ -26,7 +26,10 @@ abstract class ConvertToScopeIntention<TExpression : KtExpression>(
text: String
) : SelfTargetingIntention<TExpression>(elementType, text) {
protected val BLACKLIST_RECEIVER_NAME = listOf("this", "it")
abstract val isParameterScopeFunction: Boolean
protected val scopeReceiverName: String
get() = if (isParameterScopeFunction) "it" else "this"
protected abstract fun createScopeExpression(factory: KtPsiFactory, element: TExpression): KtExpression?
@@ -54,7 +57,7 @@ abstract class ConvertToScopeIntention<TExpression : KtExpression>(
if (expression is KtProperty) expression
else findFirstExpressionToMove(receiverExpressionText, expression)
val firstExpressionToMove = if (expression is KtProperty) expression.nextSibling else firstTargetExpression
blockExpression.moveRangeInto(firstExpressionToMove, lastExpressionToMove)
blockExpression.moveRangeInto(firstExpressionToMove, lastExpressionToMove, factory)
parent.addBefore(scopeBlockExpression, firstTargetExpression)
parent.deleteChildRange(firstTargetExpression, lastExpressionToMove)
@@ -71,14 +74,19 @@ abstract class ConvertToScopeIntention<TExpression : KtExpression>(
lambdaArguments.firstOrNull()?.getLambdaExpression()?.bodyExpression
private fun KtBlockExpression.moveRangeInto(
firstElement: PsiElement, lastElement: PsiElement
firstElement: PsiElement, lastElement: PsiElement, psiFactory: KtPsiFactory
) {
addRange(firstElement, lastElement)
children.filterIsInstance(KtDotQualifiedExpression::class.java)
.forEach { it.deleteFirstReceiver() }
.forEach {
val replaced = it.deleteFirstReceiver()
if (isParameterScopeFunction) {
replaced.replace(psiFactory.createExpressionByPattern("$scopeReceiverName.$0", replaced))
}
}
}
private fun KtCallExpression.isApplicable() = lambdaArguments.isEmpty() && valueArguments.all { it.text !in BLACKLIST_RECEIVER_NAME }
private fun KtCallExpression.isApplicable() = lambdaArguments.isEmpty() && valueArguments.all { it.text != scopeReceiverName }
private fun findFirstExpressionToMove(receiverExpressionText: String, expression: KtExpression) =
findBoundaryExpression(receiverExpressionText, expression, forward = false)
+1
View File
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.ConvertToAlsoIntention
+15
View File
@@ -0,0 +1,15 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1() = Unit
fun foo2(a: MyClass) = Unit
fun foo3() = Unit
fun foo4(it: MyClass) {
val a = MyClass()
a.foo1()
a.foo2(it)<caret>
a.foo3()
}
}
+15
View File
@@ -0,0 +1,15 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1() = Unit
fun foo2(a: MyClass) = Unit
fun foo3() = Unit
fun foo4(it: MyClass) {
val a = MyClass()
a.foo1()
a.foo2(it)
a.foo3()<caret>
}
}
+14
View File
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1(): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4() {
val a = MyClass()
a.foo1().foo2().foo3()
a.foo2()<caret>
a.foo3()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1(): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4() {
val a = MyClass().also {
it.foo1().foo2().foo3()
it.foo2()
it.foo3()
}
}
}
@@ -0,0 +1,17 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1(a: MyClass): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4() {
listOf<MyClass>().forEach {
val a = MyClass()
a.foo1(it).foo2().foo3()
a.foo2()<caret>
a.foo3()
}
}
}
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1(a: MyClass): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4() {
val a = MyClass()
a.foo1(this).foo2().foo3()
a.foo2()<caret>
a.foo3()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1(a: MyClass): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4() {
val a = MyClass().also {
it.foo1(this).foo2().foo3()
it.foo2()
it.foo3()
}
}
}
+14
View File
@@ -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()
}
}
+15
View File
@@ -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()
}
}
}
+14
View File
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
val a = MyClass()
a.foo1()
a.foo2()<caret>
a.foo3()
}
}
+15
View File
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
val a = MyClass().also {
it.foo1()
it.foo2()
it.foo3()
}
}
}
+14
View File
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
val a = MyClass()
a.foo1()
a.foo2()
a.foo3()<caret>
}
}
+15
View File
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
val a = MyClass().also {
it.foo1()
it.foo2()
it.foo3()
}
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2(a: MyClass) = Unit
fun foo3() = Unit
fun foo4(it: MyClass) {
val a = MyClass()
a.foo1()
a.foo3()<caret>
a.foo2(it)
a.foo3()
}
}
@@ -0,0 +1,16 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2(a: MyClass) = Unit
fun foo3() = Unit
fun foo4(it: MyClass) {
val a = MyClass().also {
it.foo1()
it.foo3()
}
a.foo2(it)
a.foo3()
}
}
+14
View File
@@ -0,0 +1,14 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
var a = MyClass()
a.foo1()<caret>
a.foo2()
a.foo3()
}
}
+15
View File
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4() {
var a = MyClass().also {
it.foo1()
it.foo2()
it.foo3()
}
}
}
@@ -0,0 +1,20 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
// top comment
val a = MyClass()
// here is comment
a.foo1()<caret>
// comment
// bbb
a.foo2()
a.foo3()
// last comment won't be in
}
}
@@ -0,0 +1,21 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
// top comment
val a = MyClass().also {
// here is comment
it.foo1()
// comment
// bbb
it.foo2()
it.foo3()
}
// last comment won't be in
}
}
@@ -1,5 +1,4 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1(a: MyClass): MyClass = this
@@ -0,0 +1,17 @@
// WITH_RUNTIME
class MyClass {
fun foo1(a: MyClass): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4() {
listOf<MyClass>().forEach {
val a = MyClass().apply {
foo1(it).foo2().foo3()
foo2()
foo3()
}
}
}
}
+4 -7
View File
@@ -1,16 +1,13 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
a.let {
it.foo1()<caret>
it.foo2()
it.foo3()
}
fun foo4(it: MyClass) {
it.foo1()<caret>
it.foo2()
it.foo3()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(it: MyClass) {
it.run {
foo1()
foo2()
foo3()
}
}
}
@@ -1,5 +1,4 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1(a: MyClass): MyClass = this
@@ -0,0 +1,16 @@
// WITH_RUNTIME
class MyClass {
fun foo1(a: MyClass): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4(a: MyClass) {
listOf<MyClass>().forEach {
a.run {
foo1(it).foo2().foo3()
foo2()
}
}
}
}
+4 -7
View File
@@ -1,16 +1,13 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(a: MyClass) {
a.let {
it.foo1()<caret>
it.foo2()
it.foo3()
}
fun foo4(it: MyClass) {
it.foo1()<caret>
it.foo2()
it.foo3()
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
class MyClass {
fun foo1() = Unit
fun foo2() = Unit
fun foo3() = Unit
fun foo4(it: MyClass) {
with(it) {
foo1()
foo2()
foo3()
}
}
}
@@ -1,5 +1,4 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class MyClass {
fun foo1(a: MyClass): MyClass = this
@@ -0,0 +1,16 @@
// WITH_RUNTIME
class MyClass {
fun foo1(a: MyClass): MyClass = this
fun foo2(): MyClass = this
fun foo3(): MyClass = this
fun foo4(a: MyClass) {
listOf<MyClass>().forEach {
with(a) {
foo1(it).foo2().foo3()
foo2()
}
}
}
}
@@ -7004,6 +7004,74 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/convertToAlso")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConvertToAlso extends AbstractIntentionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInConvertToAlso() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertToAlso"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("itParameter.kt")
public void testItParameter() throws Exception {
runTest("idea/testData/intentions/convertToAlso/itParameter.kt");
}
@TestMetadata("itParameter2.kt")
public void testItParameter2() throws Exception {
runTest("idea/testData/intentions/convertToAlso/itParameter2.kt");
}
@TestMetadata("methodChain.kt")
public void testMethodChain() throws Exception {
runTest("idea/testData/intentions/convertToAlso/methodChain.kt");
}
@TestMetadata("methodChainWithItParameter.kt")
public void testMethodChainWithItParameter() throws Exception {
runTest("idea/testData/intentions/convertToAlso/methodChainWithItParameter.kt");
}
@TestMetadata("methodChainWithThisParameter.kt")
public void testMethodChainWithThisParameter() throws Exception {
runTest("idea/testData/intentions/convertToAlso/methodChainWithThisParameter.kt");
}
@TestMetadata("normal.kt")
public void testNormal() throws Exception {
runTest("idea/testData/intentions/convertToAlso/normal.kt");
}
@TestMetadata("normal2.kt")
public void testNormal2() throws Exception {
runTest("idea/testData/intentions/convertToAlso/normal2.kt");
}
@TestMetadata("normal3.kt")
public void testNormal3() throws Exception {
runTest("idea/testData/intentions/convertToAlso/normal3.kt");
}
@TestMetadata("untilItParameter.kt")
public void testUntilItParameter() throws Exception {
runTest("idea/testData/intentions/convertToAlso/untilItParameter.kt");
}
@TestMetadata("var.kt")
public void testVar() throws Exception {
runTest("idea/testData/intentions/convertToAlso/var.kt");
}
@TestMetadata("withCommentAndSpaces.kt")
public void testWithCommentAndSpaces() throws Exception {
runTest("idea/testData/intentions/convertToAlso/withCommentAndSpaces.kt");
}
}
@TestMetadata("idea/testData/intentions/convertToApply")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)