[kotlin] implement "Put calls on separate lines" intention

^KTIJ-24022 Fixed

GitOrigin-RevId: f487e6878fa180d123890987251396ead63ddeff
This commit is contained in:
Dmitrii Gridin
2022-12-20 13:12:22 +00:00
committed by intellij-monorepo-bot
parent 79d667b484
commit ce7d3767a0
35 changed files with 365 additions and 0 deletions
@@ -1687,6 +1687,7 @@ convert.to.lambda.expression=Convert to a lambda expression
put.arguments.on.separate.lines=Put arguments on separate lines
put.parameters.on.separate.lines=Put parameters on separate lines
put.expressions.on.separate.lines=Put expressions on separate lines
put.calls.on.separate.lines=Put calls on separate lines
demorgan.law=DeMorgan Law
replace.with.end.of.line.comment=Replace with end of line comment
convert.collection.constructor.to.function=Convert Collection constructor to function
@@ -0,0 +1,7 @@
fun calculate(): Int {
return call1().call2()
.call3()
.call5()
.call6()
.call7()
}
@@ -0,0 +1,3 @@
fun calculate(): Int {
return call1().call2().call3()<spot>.</spot>call5().call6().call7()
}
@@ -0,0 +1,5 @@
<html>
<body>
Formats call chain by placing each call on a separate line.
</body>
</html>
@@ -98,6 +98,13 @@
<categoryKey>group.names.kotlin</categoryKey>
</intentionAction>
<intentionAction>
<language>kotlin</language>
<className>org.jetbrains.kotlin.idea.codeInsight.intentions.shared.PutCallsOnSeparateLinesIntention</className>
<bundleName>messages.KotlinBundle</bundleName>
<categoryKey>group.names.kotlin</categoryKey>
</intentionAction>
<intentionAction>
<language>kotlin</language>
<className>org.jetbrains.kotlin.idea.codeInsight.intentions.shared.JoinArgumentListIntention</className>
@@ -0,0 +1,78 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.codeInsight.intentions.shared
import com.intellij.application.options.CodeStyle
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.psi.impl.source.PostprocessReformattingAspect
import com.intellij.psi.util.descendants
import com.intellij.psi.util.parents
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.intentions.AbstractKotlinApplicableIntention
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.KotlinApplicabilityRange
import org.jetbrains.kotlin.idea.codeinsight.api.applicators.applicabilityRange
import org.jetbrains.kotlin.idea.formatter.kotlinCommonSettings
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.util.takeWhileIsInstance
internal class PutCallsOnSeparateLinesIntention :
AbstractKotlinApplicableIntention<KtQualifiedExpression>(KtQualifiedExpression::class) {
override fun getActionName(element: KtQualifiedExpression): String = familyName
override fun getFamilyName(): String = KotlinBundle.message("put.calls.on.separate.lines")
override fun apply(element: KtQualifiedExpression, project: Project, editor: Editor?) {
val rootQualifierExpression = element.topmostQualifierExpression() ?: return
PostprocessReformattingAspect.getInstance(project).disablePostprocessFormattingInside {
val psiFactory = KtPsiFactory(project)
rootQualifierExpression.visitOperations(transformation = callChainTransformation(element)) { qualifierExpression ->
val operationReference = qualifierExpression.operationTokenNode as? PsiElement ?: return@visitOperations
val whiteSpace = operationReference.prevSibling as? PsiWhiteSpace
when {
whiteSpace == null -> qualifierExpression.addBefore(psiFactory.createNewLine(), operationReference)
!whiteSpace.textContains('\n') -> whiteSpace.replace(psiFactory.createWhiteSpace("\n${whiteSpace.text}"))
}
}
}
CodeStyleManager.getInstance(project).reformat(/* element = */ rootQualifierExpression, /* canChangeWhiteSpacesOnly = */ true)
}
override fun getApplicabilityRange(): KotlinApplicabilityRange<KtQualifiedExpression> = applicabilityRange {
(it.operationTokenNode as? PsiElement)?.textRangeInParent
}
override fun isApplicableByPsi(element: KtQualifiedExpression): Boolean {
val topmostQualifierExpression = element.topmostQualifierExpression() ?: return false
if (topmostQualifierExpression.parent is KtImportDirective) return false
topmostQualifierExpression.visitOperations(transformation = callChainTransformation(element)) {
val nextSibling = it.operationTokenNode.treePrev as? PsiWhiteSpace ?: return true
if (!nextSibling.textContains('\n')) return true
}
return false
}
private fun callChainTransformation(element: PsiElement): Sequence<KtQualifiedExpression>.() -> Sequence<KtQualifiedExpression> {
val wrapFirstCall = CodeStyle.getSettings(element.containingFile).kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN
return { if (wrapFirstCall) this else drop(1) }
}
}
private inline fun KtQualifiedExpression.visitOperations(
transformation: Sequence<KtQualifiedExpression>.() -> Sequence<KtQualifiedExpression>,
action: (KtQualifiedExpression) -> Unit,
) {
descendants(childrenFirst = true) { it is KtQualifiedExpression }
.filterIsInstance<KtQualifiedExpression>()
.transformation()
.forEach(action)
}
private fun KtQualifiedExpression.topmostQualifierExpression(): KtQualifiedExpression? =
parents(withSelf = true).takeWhileIsInstance<KtQualifiedExpression>().lastOrNull()
@@ -445,6 +445,69 @@ public abstract class SharedK1IntentionTestGenerated extends AbstractSharedK1Int
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/intentions/putCallsOnSeparateLines")
public static class PutCallsOnSeparateLines extends AbstractSharedK1IntentionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("doNotWrapFirstElement.kt")
public void testDoNotWrapFirstElement() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/doNotWrapFirstElement.kt");
}
@TestMetadata("doNotWrapFirstElement2.kt")
public void testDoNotWrapFirstElement2() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/doNotWrapFirstElement2.kt");
}
@TestMetadata("doNotWrapFirstElement3.kt")
public void testDoNotWrapFirstElement3() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/doNotWrapFirstElement3.kt");
}
@TestMetadata("dotChain.kt")
public void testDotChain() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/dotChain.kt");
}
@TestMetadata("dotChainWithParenthesis.kt")
public void testDotChainWithParenthesis() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/dotChainWithParenthesis.kt");
}
@TestMetadata("import.kt")
public void testImport() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/import.kt");
}
@TestMetadata("mixWithMultilineBody.kt")
public void testMixWithMultilineBody() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/mixWithMultilineBody.kt");
}
@TestMetadata("mixWithMultilineBodyInFirstPosition.kt")
public void testMixWithMultilineBodyInFirstPosition() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/mixWithMultilineBodyInFirstPosition.kt");
}
@TestMetadata("safeChain.kt")
public void testSafeChain() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/safeChain.kt");
}
@TestMetadata("safeChainWithLineBreaks.kt")
public void testSafeChainWithLineBreaks() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/safeChainWithLineBreaks.kt");
}
@TestMetadata("safeChainWithOneLineBreak.kt")
public void testSafeChainWithOneLineBreak() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/safeChainWithOneLineBreak.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/intentions/putExpressionsOnSeparateLines")
public static class PutExpressionsOnSeparateLines extends AbstractSharedK1IntentionTest {
@@ -445,6 +445,69 @@ public abstract class SharedK2IntentionTestGenerated extends AbstractSharedK2Int
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/intentions/putCallsOnSeparateLines")
public static class PutCallsOnSeparateLines extends AbstractSharedK2IntentionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("doNotWrapFirstElement.kt")
public void testDoNotWrapFirstElement() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/doNotWrapFirstElement.kt");
}
@TestMetadata("doNotWrapFirstElement2.kt")
public void testDoNotWrapFirstElement2() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/doNotWrapFirstElement2.kt");
}
@TestMetadata("doNotWrapFirstElement3.kt")
public void testDoNotWrapFirstElement3() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/doNotWrapFirstElement3.kt");
}
@TestMetadata("dotChain.kt")
public void testDotChain() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/dotChain.kt");
}
@TestMetadata("dotChainWithParenthesis.kt")
public void testDotChainWithParenthesis() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/dotChainWithParenthesis.kt");
}
@TestMetadata("import.kt")
public void testImport() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/import.kt");
}
@TestMetadata("mixWithMultilineBody.kt")
public void testMixWithMultilineBody() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/mixWithMultilineBody.kt");
}
@TestMetadata("mixWithMultilineBodyInFirstPosition.kt")
public void testMixWithMultilineBodyInFirstPosition() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/mixWithMultilineBodyInFirstPosition.kt");
}
@TestMetadata("safeChain.kt")
public void testSafeChain() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/safeChain.kt");
}
@TestMetadata("safeChainWithLineBreaks.kt")
public void testSafeChainWithLineBreaks() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/safeChainWithLineBreaks.kt");
}
@TestMetadata("safeChainWithOneLineBreak.kt")
public void testSafeChainWithOneLineBreak() throws Exception {
runTest("../testData/intentions/putCallsOnSeparateLines/safeChainWithOneLineBreak.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../testData/intentions/putExpressionsOnSeparateLines")
public static class PutExpressionsOnSeparateLines extends AbstractSharedK2IntentionTest {
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.codeInsight.intentions.shared.PutCallsOnSeparateLinesIntention
@@ -0,0 +1,6 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1().call2().call3()<caret>.call5().ref6.call7()
}
// SET_TRUE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,11 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()
.call2()
.call3()
.call5()
.ref6
.call7()
}
// SET_TRUE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,6 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()<caret>.call2()
}
// SET_TRUE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,7 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()
.call2()
}
// SET_TRUE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,8 @@
// IS_APPLICABLE: false
// DISABLE-ERRORS
fun calculate(): Int {
return call1()
<caret>.call2()
}
// SET_TRUE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,4 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1().call2().call3()<caret>.call5().ref6.call7()
}
@@ -0,0 +1,8 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1().call2()
.call3()
.call5()
.ref6
.call7()
}
@@ -0,0 +1,4 @@
// DISABLE-ERRORS
fun calculate(): Int {
return (call1().call2().call3()<caret>.call5()).ref6.call7()
}
@@ -0,0 +1,6 @@
// DISABLE-ERRORS
fun calculate(): Int {
return (call1().call2()
.call3()
.call5()).ref6.call7()
}
@@ -0,0 +1,4 @@
// IS_APPLICABLE: false
// DISABLE-ERRORS
import a.n<caret>.c.d
@@ -0,0 +1,6 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1().call2().call3()<caret>.call5 {
doSmth()
}?.ref6?.call7()
}
@@ -0,0 +1,10 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1().call2()
.call3()
.call5 {
doSmth()
}
?.ref6
?.call7()
}
@@ -0,0 +1,6 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()<caret>.call5 {
doSmth()
}?.ref6?.call7()
}
@@ -0,0 +1,8 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1().call5 {
doSmth()
}
?.ref6
?.call7()
}
@@ -0,0 +1,4 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()?.call2()?.call3()?<caret>.call5()?.ref6?.call7()
}
@@ -0,0 +1,8 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()?.call2()
?.call3()
?.call5()
?.ref6
?.call7()
}
@@ -0,0 +1,9 @@
// IS_APPLICABLE: false
// DISABLE-ERRORS
fun calculate(): Int {
return call1()?.call2()
?.call3()
?<caret>.call5()
?.ref6
?.call7()
}
@@ -0,0 +1,7 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()?.call2()
?.call3()
?<caret>.call5()?.ref6
?.call7()
}
@@ -0,0 +1,8 @@
// DISABLE-ERRORS
fun calculate(): Int {
return call1()?.call2()
?.call3()
?.call5()
?.ref6
?.call7()
}
@@ -1,5 +1,6 @@
// "Create class 'A'" "false"
// ACTION: Introduce local variable
// ACTION: Put calls on separate lines
// ACTION: Rename reference
// ERROR: Unresolved reference: A
fun foo() = J.<caret>A.B
@@ -1,6 +1,7 @@
// "Replace with 'declaringJavaClass'" "true"
// ACTION: Add method contract to 'getDeclaringClass()'
// ACTION: Introduce local variable
// ACTION: Put calls on separate lines
// ACTION: Replace with 'declaringJavaClass'
// API_VERSION: 1.7
// WITH_STDLIB
@@ -1,6 +1,7 @@
// "Replace with 'declaringJavaClass'" "true"
// ACTION: Add method contract to 'getDeclaringClass()'
// ACTION: Introduce local variable
// ACTION: Put calls on separate lines
// ACTION: Replace with 'declaringJavaClass'
// API_VERSION: 1.7
// WITH_STDLIB
@@ -1,4 +1,5 @@
// "Introduce import alias" "false"
// ACTION: Put calls on separate lines
// WITH_STDLIB
// ERROR: Overload resolution ambiguity: <br>public inline fun <T> Iterable<TypeVariable(T)>.forEach(action: (TypeVariable(T)) -> Unit): Unit defined in kotlin.collections<br>public inline fun <K, V> Map<out TypeVariable(K), TypeVariable(V)>.forEach(action: (Map.Entry<TypeVariable(K), TypeVariable(V)>) -> Unit): Unit defined in kotlin.collections
// ERROR: Unresolved reference: a
@@ -1,6 +1,7 @@
// "Surround with null check" "false"
// ACTION: Add non-null asserted (!!) call
// ACTION: Introduce local variable
// ACTION: Put calls on separate lines
// ACTION: Replace with safe (?.) call
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?
@@ -2,6 +2,7 @@
// WITH_STDLIB
// ACTION: Add non-null asserted (!!) call
// ACTION: Introduce local variable
// ACTION: Put calls on separate lines
// ACTION: Replace with safe (?.) call
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?
@@ -2,6 +2,7 @@
// ACTION: Add non-null asserted (!!) call
// ACTION: Convert to run
// ACTION: Convert to with
// ACTION: Put calls on separate lines
// ACTION: Replace with safe (?.) call
// ACTION: Surround with null check
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type B?