Update diagnostics for trailing lambdas, add quickfix

Alternative message for errors, caused by unexpected lambda expression arguments on a new line.
Both diagnostic are reported, if multiple lambda expressions were passed to the call.
For other errors trailing lambda diagnostic overrides the original one.

Quickfix for erroneous trailing lambdas on a new line after call.
Fix separates lambda expression from previous call with semicolon.
All trailing lambda arguments become standalone lambda expressions.
This commit is contained in:
Pavel Kirpichenkov
2019-09-18 12:04:42 +03:00
parent f00d609459
commit 6c8e829f19
21 changed files with 390 additions and 9 deletions
@@ -0,0 +1,76 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.quickfix
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.quickfix.quickfixUtil.createIntentionForFirstParentOfType
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
class AddSemicolonBeforeLambdaExpressionFix(element: KtLambdaExpression) : KotlinQuickFixAction<KtLambdaExpression>(element) {
override fun getText(): String = "Terminate preceding call with semicolon"
override fun getFamilyName(): String = text
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val lambdaExpressionArgument = element?.parent?.safeAs<KtLambdaArgument>()
?: return
val callExpression = lambdaExpressionArgument.parent.safeAs<KtCallExpression>()
?: return
val desiredEndOfCallExpression =
PsiTreeUtil.findSiblingBackward(
lambdaExpressionArgument,
KtNodeTypes.LAMBDA_ARGUMENT,
null
) ?: PsiTreeUtil.findSiblingBackward(
lambdaExpressionArgument,
KtNodeTypes.VALUE_ARGUMENT_LIST,
null
)
desiredEndOfCallExpression?.let { endOfCall ->
makeNewExpressionsFromFollowingLambdas(callExpression, endOfCall)
val semicolon = callExpression.parent.addAfter(
KtPsiFactory(project).createSemicolon(),
callExpression
)
editor?.caretModel?.moveToOffset(semicolon.startOffset)
}
}
private fun makeNewExpressionsFromFollowingLambdas(
oldCallExpression: KtCallExpression,
endOfArguments: PsiElement
) {
var lastSibling = oldCallExpression.lastChild
val parentForCallExpression = oldCallExpression.parent
while (lastSibling != endOfArguments) {
when (lastSibling) {
is KtLambdaArgument -> parentForCallExpression.addAfter(
lastSibling.getLambdaExpression() ?: lastSibling,
oldCallExpression
)
else -> parentForCallExpression.addAfter(
lastSibling,
oldCallExpression
)
}
lastSibling = lastSibling.prevSibling
}
oldCallExpression.deleteChildRange(endOfArguments.nextSibling, oldCallExpression.lastChild)
}
companion object Factory : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic) =
diagnostic.createIntentionForFirstParentOfType(::AddSemicolonBeforeLambdaExpressionFix)
}
}
@@ -611,5 +611,7 @@ class QuickFixRegistrar : QuickFixContributor {
RESTRICTED_RETENTION_FOR_EXPRESSION_ANNOTATION_WARNING.registerFactory(RestrictedRetentionForExpressionAnnotationFactory)
NO_VALUE_FOR_PARAMETER.registerFactory(AddConstructorParameterFromSuperTypeCallFix)
UNEXPECTED_TRAILING_LAMBDA_ON_A_NEW_LINE.registerFactory(AddSemicolonBeforeLambdaExpressionFix.Factory)
}
}
@@ -0,0 +1,8 @@
// "Terminate preceding call with semicolon" "true"
fun foo() {}
fun test() {
foo()
{<caret>}
}
@@ -0,0 +1,8 @@
// "Terminate preceding call with semicolon" "true"
fun foo() {}
fun test() {
foo()<caret>;
{}
}
@@ -0,0 +1,12 @@
// "Terminate preceding call with semicolon" "true"
fun foo(
fn: () -> Unit
) {}
fun test() {
foo()
{}
{}<caret>
{}
}
@@ -0,0 +1,12 @@
// "Terminate preceding call with semicolon" "true"
fun foo(
fn: () -> Unit
) {}
fun test() {
foo()
{}<caret>;
{}
{}
}
@@ -0,0 +1,12 @@
// "Terminate preceding call with semicolon" "true"
fun foo() {}
fun test() {
foo()/*
block
comment
*/
// comment
{}<caret>
}
@@ -0,0 +1,12 @@
// "Terminate preceding call with semicolon" "true"
fun foo() {}
fun test() {
foo()<caret>;/*
block
comment
*/
// comment
{}
}
@@ -312,6 +312,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
}
}
@TestMetadata("idea/testData/quickfix/addSemicolonBeforeLambdaExpression")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddSemicolonBeforeLambdaExpression extends AbstractQuickFixMultiFileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInAddSemicolonBeforeLambdaExpression() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addSemicolonBeforeLambdaExpression"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
}
}
@TestMetadata("idea/testData/quickfix/addStarProjections")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -1097,6 +1097,34 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/addSemicolonBeforeLambdaExpression")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddSemicolonBeforeLambdaExpression extends AbstractQuickFixTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInAddSemicolonBeforeLambdaExpression() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addSemicolonBeforeLambdaExpression"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("basic.kt")
public void testBasic() throws Exception {
runTest("idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt");
}
@TestMetadata("multipleLambdas.kt")
public void testMultipleLambdas() throws Exception {
runTest("idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt");
}
@TestMetadata("withComments.kt")
public void testWithComments() throws Exception {
runTest("idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt");
}
}
@TestMetadata("idea/testData/quickfix/addStarProjections")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)