From 6c8e829f191b7fee0aa80b969d7b6d76b51d8553 Mon Sep 17 00:00:00 2001 From: Pavel Kirpichenkov Date: Wed, 18 Sep 2019 12:04:42 +0300 Subject: [PATCH] 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. --- .../fir/FirDiagnosticsSmokeTestGenerated.java | 5 + .../jetbrains/kotlin/diagnostics/Errors.java | 1 + .../rendering/DefaultErrorMessages.java | 1 + .../DiagnosticReporterByTrackingStrategy.kt | 10 +- .../ValueArgumentsToParametersMapper.java | 22 +++- .../resolve/calls/tower/PSICallResolver.kt | 3 + .../kotlin/resolve/calls/util/callUtil.kt | 35 +++++- .../tests/resolve/newLineLambda.kt | 118 ++++++++++++++++++ .../tests/resolve/newLineLambda.txt | 11 ++ .../checkers/DiagnosticsTestGenerated.java | 5 + .../DiagnosticsUsingJavacTestGenerated.java | 5 + .../AddSemicolonBeforeLambdaExpressionFix.kt | 76 +++++++++++ .../kotlin/idea/quickfix/QuickFixRegistrar.kt | 2 + .../basic.kt | 8 ++ .../basic.kt.after | 8 ++ .../multipleLambdas.kt | 12 ++ .../multipleLambdas.kt.after | 12 ++ .../withComments.kt | 12 ++ .../withComments.kt.after | 12 ++ .../QuickFixMultiFileTestGenerated.java | 13 ++ .../idea/quickfix/QuickFixTestGenerated.java | 28 +++++ 21 files changed, 390 insertions(+), 9 deletions(-) create mode 100644 compiler/testData/diagnostics/tests/resolve/newLineLambda.kt create mode 100644 compiler/testData/diagnostics/tests/resolve/newLineLambda.txt create mode 100644 idea/src/org/jetbrains/kotlin/idea/quickfix/AddSemicolonBeforeLambdaExpressionFix.kt create mode 100644 idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt create mode 100644 idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt.after create mode 100644 idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt create mode 100644 idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt.after create mode 100644 idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt create mode 100644 idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt.after diff --git a/compiler/fir/resolve/tests/org/jetbrains/kotlin/fir/FirDiagnosticsSmokeTestGenerated.java b/compiler/fir/resolve/tests/org/jetbrains/kotlin/fir/FirDiagnosticsSmokeTestGenerated.java index cbcaf8768bf..4e9a2fc3347 100644 --- a/compiler/fir/resolve/tests/org/jetbrains/kotlin/fir/FirDiagnosticsSmokeTestGenerated.java +++ b/compiler/fir/resolve/tests/org/jetbrains/kotlin/fir/FirDiagnosticsSmokeTestGenerated.java @@ -17591,6 +17591,11 @@ public class FirDiagnosticsSmokeTestGenerated extends AbstractFirDiagnosticsSmok runTest("compiler/testData/diagnostics/tests/resolve/localObject.kt"); } + @TestMetadata("newLineLambda.kt") + public void testNewLineLambda() throws Exception { + runTest("compiler/testData/diagnostics/tests/resolve/newLineLambda.kt"); + } + @TestMetadata("objectLiteralAsArgument.kt") public void testObjectLiteralAsArgument() throws Exception { runTest("compiler/testData/diagnostics/tests/resolve/objectLiteralAsArgument.kt"); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java index 3d0bb60d00f..a761c14fff3 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java @@ -695,6 +695,7 @@ public interface Errors { DiagnosticFactory0 SPREAD_OF_LAMBDA_OR_CALLABLE_REFERENCE = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 MANY_LAMBDA_EXPRESSION_ARGUMENTS = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 UNEXPECTED_TRAILING_LAMBDA_ON_A_NEW_LINE = DiagnosticFactory0.create(ERROR); DiagnosticFactory1 TOO_MANY_ARGUMENTS = DiagnosticFactory1.create(ERROR); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java index 2fb829ccde2..d9ec4596688 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java @@ -203,6 +203,7 @@ public class DefaultErrorMessages { MAP.put(SPREAD_OF_LAMBDA_OR_CALLABLE_REFERENCE, "The spread operator (*foo) cannot be applied to lambda argument or callable reference"); MAP.put(MANY_LAMBDA_EXPRESSION_ARGUMENTS, "Only one lambda expression is allowed outside a parenthesized argument list"); + MAP.put(UNEXPECTED_TRAILING_LAMBDA_ON_A_NEW_LINE, "Expression is treated a trailing lambda argument; consider separating it from call with semicolon"); MAP.put(PROPERTY_WITH_NO_TYPE_NO_INITIALIZER, "This property must either have a type annotation, be initialized or be delegated"); MAP.put(VARIABLE_WITH_NO_TYPE_NO_INITIALIZER, "This variable must either have a type annotation or be initialized"); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/DiagnosticReporterByTrackingStrategy.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/DiagnosticReporterByTrackingStrategy.kt index 2daa47dfa2b..20510b7cdfb 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/DiagnosticReporterByTrackingStrategy.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/DiagnosticReporterByTrackingStrategy.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.isNull import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.callUtil.reportTrailingLambdaErrorOr import org.jetbrains.kotlin.resolve.calls.context.BasicCallResolutionContext import org.jetbrains.kotlin.resolve.calls.inference.model.* import org.jetbrains.kotlin.resolve.calls.model.* @@ -118,14 +119,15 @@ class DiagnosticReporterByTrackingStrategy( SmartCastDiagnostic::class.java -> reportSmartCast(diagnostic as SmartCastDiagnostic) UnstableSmartCast::class.java -> reportUnstableSmartCast(diagnostic as UnstableSmartCast) TooManyArguments::class.java -> { - reportIfNonNull(callArgument.psiExpression) { - trace.report(TOO_MANY_ARGUMENTS.on(it, (diagnostic as TooManyArguments).descriptor)) + trace.reportTrailingLambdaErrorOr(callArgument.psiExpression) { expr -> + TOO_MANY_ARGUMENTS.on(expr, (diagnostic as TooManyArguments).descriptor) } trace.markAsReported() } - VarargArgumentOutsideParentheses::class.java -> - reportIfNonNull(callArgument.psiExpression) { trace.report(VARARG_OUTSIDE_PARENTHESES.on(it)) } + VarargArgumentOutsideParentheses::class.java -> trace.reportTrailingLambdaErrorOr(callArgument.psiExpression) { expr -> + VARARG_OUTSIDE_PARENTHESES.on(expr) + } MixingNamedAndPositionArguments::class.java -> trace.report(MIXING_NAMED_AND_POSITIONED_ARGUMENTS.on(callArgument.psiCallArgument.valueArgument.asElement())) diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/ValueArgumentsToParametersMapper.java b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/ValueArgumentsToParametersMapper.java index f211a1f6932..3a0ae7dc46f 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/ValueArgumentsToParametersMapper.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/ValueArgumentsToParametersMapper.java @@ -278,17 +278,26 @@ public class ValueArgumentsToParametersMapper { KtExpression possiblyLabeledFunctionLiteral = lambdaArgument.getArgumentExpression(); if (parameters.isEmpty()) { - report(TOO_MANY_ARGUMENTS.on(possiblyLabeledFunctionLiteral, candidateCall.getCandidateDescriptor())); + CallUtilKt.reportTrailingLambdaErrorOr( + candidateCall.getTrace(), possiblyLabeledFunctionLiteral, + expression -> TOO_MANY_ARGUMENTS.on(expression, candidateCall.getCandidateDescriptor()) + ); setStatus(ERROR); } else { ValueParameterDescriptor lastParameter = CollectionsKt.last(parameters); if (lastParameter.getVarargElementType() != null) { - report(VARARG_OUTSIDE_PARENTHESES.on(possiblyLabeledFunctionLiteral)); + CallUtilKt.reportTrailingLambdaErrorOr( + candidateCall.getTrace(), possiblyLabeledFunctionLiteral, + expression -> VARARG_OUTSIDE_PARENTHESES.on(expression) + ); setStatus(ERROR); } else if (!usedParameters.add(lastParameter)) { - report(TOO_MANY_ARGUMENTS.on(possiblyLabeledFunctionLiteral, candidateCall.getCandidateDescriptor())); + CallUtilKt.reportTrailingLambdaErrorOr( + candidateCall.getTrace(), possiblyLabeledFunctionLiteral, + expr -> TOO_MANY_ARGUMENTS.on(expr, candidateCall.getCandidateDescriptor()) + ); setStatus(WEAK_ERROR); } else { @@ -298,7 +307,12 @@ public class ValueArgumentsToParametersMapper { for (int i = 1; i < functionLiteralArguments.size(); i++) { KtExpression argument = functionLiteralArguments.get(i).getArgumentExpression(); - report(MANY_LAMBDA_EXPRESSION_ARGUMENTS.on(argument)); + if (argument instanceof KtLambdaExpression) { + report(MANY_LAMBDA_EXPRESSION_ARGUMENTS.on(argument)); + if (CallUtilKt.isTrailingLambdaOnNewLIne((KtLambdaExpression) argument)) { + report(UNEXPECTED_TRAILING_LAMBDA_ON_A_NEW_LINE.on((KtLambdaExpression) argument)); + } + } setStatus(WEAK_ERROR); } } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/tower/PSICallResolver.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/tower/PSICallResolver.kt index 0b9b5be438c..f9363e5491e 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/tower/PSICallResolver.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/tower/PSICallResolver.kt @@ -538,6 +538,9 @@ class PSICallResolver( if (i == 0) continue val lambdaExpression = externalLambdaArguments[i].getLambdaExpression() ?: continue + if (lambdaExpression.isTrailingLambdaOnNewLIne) { + context.trace.report(Errors.UNEXPECTED_TRAILING_LAMBDA_ON_A_NEW_LINE.on(lambdaExpression)) + } context.trace.report(Errors.MANY_LAMBDA_EXPRESSION_ARGUMENTS.on(lambdaExpression)) } } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/util/callUtil.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/util/callUtil.kt index 249696038e2..15a96da84bc 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/util/callUtil.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/util/callUtil.kt @@ -17,14 +17,18 @@ package org.jetbrains.kotlin.resolve.calls.callUtil import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.incremental.KotlinLookupLocation import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.getTextWithLocation import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext.CALL import org.jetbrains.kotlin.resolve.BindingContext.RESOLVED_CALL +import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.calls.ArgumentTypeResolver import org.jetbrains.kotlin.resolve.calls.CallTransformer import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext @@ -278,4 +282,33 @@ fun ResolvedCall<*>.getFirstArgumentExpression(): KtExpression? = valueArgumentsByIndex?.run { get(0).arguments[0].getArgumentExpression() } fun ResolvedCall<*>.getReceiverExpression(): KtExpression? = - extensionReceiver.safeAs()?.expression ?: dispatchReceiver.safeAs()?.expression \ No newline at end of file + extensionReceiver.safeAs()?.expression ?: dispatchReceiver.safeAs()?.expression + +val KtLambdaExpression.isTrailingLambdaOnNewLIne + get(): Boolean { + parent?.safeAs()?.let { lambdaArgument -> + var prevSibling = lambdaArgument.prevSibling + + while (prevSibling != null && prevSibling !is KtElement) { + if (prevSibling is PsiWhiteSpace && prevSibling.textContains('\n')) + return true + prevSibling = prevSibling.prevSibling + } + } + + return false + } + + +inline fun BindingTrace.reportTrailingLambdaErrorOr( + expression: KtExpression?, + originalDiagnostic: (KtExpression) -> Diagnostic +) { + expression?.let { expr -> + if (expr is KtLambdaExpression && expr.isTrailingLambdaOnNewLIne) { + report(Errors.UNEXPECTED_TRAILING_LAMBDA_ON_A_NEW_LINE.on(expr)) + } else { + report(originalDiagnostic(expr)) + } + } +} diff --git a/compiler/testData/diagnostics/tests/resolve/newLineLambda.kt b/compiler/testData/diagnostics/tests/resolve/newLineLambda.kt new file mode 100644 index 00000000000..d8c1eab4f37 --- /dev/null +++ b/compiler/testData/diagnostics/tests/resolve/newLineLambda.kt @@ -0,0 +1,118 @@ +// !WITH_NEW_INFERENCE +// !DIAGNOSTICS: -UNUSED_PARAMETER + +fun noArgs() {} +fun oneLambdaArg(fn: () -> Unit) {} +fun twoLambdaArgs(f1: () -> Unit, f2: () -> Unit) {} +fun varargFn(vararg args: Int) {} + +fun testNoArgs() { + noArgs() + noArgs {} + noArgs() {} + noArgs() // {} + noArgs() /* */ {} + noArgs() /* + block comment, no new line + */ {} + noArgs() + /* + block comment with new line + */ + {} + noArgs() // comment + // comment + {} + noArgs() {} {} + noArgs() {} + {} +} + +fun testLambdaArg() { + oneLambdaArg() + oneLambdaArg {} + oneLambdaArg() + {} + oneLambdaArg() + {} + {} + oneLambdaArg( + {}, + {} + ) + oneLambdaArg() {} + oneLambdaArg() // {} + oneLambdaArg() /* */ {} + oneLambdaArg() /* + block + comment + */ {} + oneLambdaArg() // comment + // comment + {} + oneLambdaArg() {}/* + block comment, no new line + */ {} + oneLambdaArg() {}/* + block comment with new line + */ + {} + oneLambdaArg() {}// comment + // comment + {} + oneLambdaArg() {} {} + oneLambdaArg() {} + {} + oneLambdaArg() {} // comment + {} +} + +fun testVararg() { + varargFn(1,2,3) + varargFn {} + varargFn(1,2,3) {} + varargFn(1,2,3) // {} + varargFn(1,2,3) /* */ {} + varargFn(1,2,3) /* + block comment, no new line + */ {} + varargFn(1,2,3) + /* + block comment with new line + */ {} + varargFn(1,2,3) // comment + // comment + {} + varargFn(1,2,3) {} {} + varargFn(1,2,3) {} + {} +} + +fun testTwoLambdas() { + twoLambdaArgs( + f1 = {}, + f2 = + {} + ) + + fun bar(): () -> Unit { + twoLambdaArgs() + {} + {} + + return if (true) { + twoLambdaArgs({}) + {} + {} + } else { + {} + } + } +} + +fun f1(): (() -> Unit) -> (() -> Unit) -> Unit { + return { l1 -> + l1() + { l2 -> l2() } + } +} diff --git a/compiler/testData/diagnostics/tests/resolve/newLineLambda.txt b/compiler/testData/diagnostics/tests/resolve/newLineLambda.txt new file mode 100644 index 00000000000..df0d23676e6 --- /dev/null +++ b/compiler/testData/diagnostics/tests/resolve/newLineLambda.txt @@ -0,0 +1,11 @@ +package + +public fun f1(): (() -> kotlin.Unit) -> (() -> kotlin.Unit) -> kotlin.Unit +public fun noArgs(): kotlin.Unit +public fun oneLambdaArg(/*0*/ fn: () -> kotlin.Unit): kotlin.Unit +public fun testLambdaArg(): kotlin.Unit +public fun testNoArgs(): kotlin.Unit +public fun testTwoLambdas(): kotlin.Unit +public fun testVararg(): kotlin.Unit +public fun twoLambdaArgs(/*0*/ f1: () -> kotlin.Unit, /*1*/ f2: () -> kotlin.Unit): kotlin.Unit +public fun varargFn(/*0*/ vararg args: kotlin.Int /*kotlin.IntArray*/): kotlin.Unit diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java index 801136fec4f..4bce66ca116 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java @@ -17603,6 +17603,11 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest { runTest("compiler/testData/diagnostics/tests/resolve/localObject.kt"); } + @TestMetadata("newLineLambda.kt") + public void testNewLineLambda() throws Exception { + runTest("compiler/testData/diagnostics/tests/resolve/newLineLambda.kt"); + } + @TestMetadata("objectLiteralAsArgument.kt") public void testObjectLiteralAsArgument() throws Exception { runTest("compiler/testData/diagnostics/tests/resolve/objectLiteralAsArgument.kt"); diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java index 82d7ce3ac34..7b805010b87 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsUsingJavacTestGenerated.java @@ -17593,6 +17593,11 @@ public class DiagnosticsUsingJavacTestGenerated extends AbstractDiagnosticsUsing runTest("compiler/testData/diagnostics/tests/resolve/localObject.kt"); } + @TestMetadata("newLineLambda.kt") + public void testNewLineLambda() throws Exception { + runTest("compiler/testData/diagnostics/tests/resolve/newLineLambda.kt"); + } + @TestMetadata("objectLiteralAsArgument.kt") public void testObjectLiteralAsArgument() throws Exception { runTest("compiler/testData/diagnostics/tests/resolve/objectLiteralAsArgument.kt"); diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/AddSemicolonBeforeLambdaExpressionFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/AddSemicolonBeforeLambdaExpressionFix.kt new file mode 100644 index 00000000000..08c3d64b91b --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/AddSemicolonBeforeLambdaExpressionFix.kt @@ -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(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() + ?: return + val callExpression = lambdaExpressionArgument.parent.safeAs() + ?: 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) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index 8d76240ee19..5676a02090a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -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) } } diff --git a/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt new file mode 100644 index 00000000000..01d368e9583 --- /dev/null +++ b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt @@ -0,0 +1,8 @@ +// "Terminate preceding call with semicolon" "true" + +fun foo() {} + +fun test() { + foo() + {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt.after b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt.after new file mode 100644 index 00000000000..71118749403 --- /dev/null +++ b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/basic.kt.after @@ -0,0 +1,8 @@ +// "Terminate preceding call with semicolon" "true" + +fun foo() {} + +fun test() { + foo(); + {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt new file mode 100644 index 00000000000..232bff4a8f5 --- /dev/null +++ b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt @@ -0,0 +1,12 @@ +// "Terminate preceding call with semicolon" "true" + +fun foo( + fn: () -> Unit +) {} + +fun test() { + foo() + {} + {} + {} +} diff --git a/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt.after b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt.after new file mode 100644 index 00000000000..c93fd6e0f1b --- /dev/null +++ b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/multipleLambdas.kt.after @@ -0,0 +1,12 @@ +// "Terminate preceding call with semicolon" "true" + +fun foo( + fn: () -> Unit +) {} + +fun test() { + foo() + {}; + {} + {} +} diff --git a/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt new file mode 100644 index 00000000000..e67076b6914 --- /dev/null +++ b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt @@ -0,0 +1,12 @@ +// "Terminate preceding call with semicolon" "true" + +fun foo() {} + +fun test() { + foo()/* + block + comment + */ + // comment + {} +} \ No newline at end of file diff --git a/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt.after b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt.after new file mode 100644 index 00000000000..9ab45266ed4 --- /dev/null +++ b/idea/testData/quickfix/addSemicolonBeforeLambdaExpression/withComments.kt.after @@ -0,0 +1,12 @@ +// "Terminate preceding call with semicolon" "true" + +fun foo() {} + +fun test() { + foo();/* + block + comment + */ + // comment + {} +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index 1ab12cb23f9..8c07814bb94 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -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) diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 8be146be202..e55ba25f76b 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -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)