From bca122e2feaa6ae4cebcd94a23696ef500705573 Mon Sep 17 00:00:00 2001 From: "alexey.afanasiev" Date: Mon, 4 Mar 2019 15:37:29 +0300 Subject: [PATCH] [groovy] IDEA-208346 Highlight illegal single argument lambda --- .../plugins/groovy/GroovyBundle.properties | 2 + .../groovy/annotator/GroovyAnnotator30.kt | 28 ++++++++++- .../AddParenthesisToLambdaParameterAction.kt | 47 +++++++++++++++++++ .../ConvertLambdaToClosureAction.kt | 22 --------- .../annotator/intentions/intentionUtil.kt | 25 ++++++++++ .../lang/psi/GroovyPsiElementFactory.java | 11 ++++- .../psi/impl/GroovyPsiElementFactoryImpl.java | 14 +++++- ...AddParenthesisToLambdaParameterTest.groovy | 41 ++++++++++++++++ .../Groovy30HighlightingTest.groovy | 4 ++ .../plugins/groovy/util/Groovy30Test.groovy | 15 ++++++ .../v30/illegalSingleArgumentLambda.groovy | 7 +++ 11 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/AddParenthesisToLambdaParameterAction.kt create mode 100644 plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/intentionUtil.kt create mode 100644 plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/AddParenthesisToLambdaParameterTest.groovy create mode 100644 plugins/groovy/test/org/jetbrains/plugins/groovy/util/Groovy30Test.groovy create mode 100644 plugins/groovy/testdata/highlighting/v30/illegalSingleArgumentLambda.groovy diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties index 8c0141495dd2..6d26c8c8f85a 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties @@ -102,6 +102,7 @@ change.modifier.not=Make ''{0}'' not {1} change.modifier.family.name=Change modifiers parameter.cast.fix=Cast {0,number,ordinal} parameter to {1} action.convert.lambda.to.closure=Convert lambda to closure +add.parenthesis.to.lambda.parameter.list=Add parenthesis to parameter list # suppress inspection "UnusedProperty" packageLocal.visibility.presentation=default visible # suppress inspection "UnusedProperty" @@ -193,6 +194,7 @@ unsupported.negated.instanceof=Negated 'instanceof' is not supported in current unsupported.elvis.assignment=Elvis assignment is not supported in current version unsupported.safe.index.access=Safe index access is not supported in current version unsupported.lambda=Lambdas are not supported in current version +illegal.single.argument.lambda=Single argument form of lambda is available only as right part of assignment expression or as argument inside method call #Override and implement method.is.not.implemented=Method ''{0}'' is not implemented diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator30.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator30.kt index ed0c8df5328c..cba1c6eeacbf 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator30.kt +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator30.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.plugins.groovy.annotator @@ -7,9 +7,16 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiModifier import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.plugins.groovy.GroovyBundle +import org.jetbrains.plugins.groovy.annotator.intentions.AddParenthesisToLambdaParameterAction import org.jetbrains.plugins.groovy.codeInspection.bugs.GrRemoveModifierFix import org.jetbrains.plugins.groovy.lang.psi.GroovyElementVisitor +import org.jetbrains.plugins.groovy.lang.psi.api.GrLambdaExpression import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierList +import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable +import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrParenthesizedExpression +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition @@ -31,5 +38,24 @@ class GroovyAnnotator30(private val holder: AnnotationHolder) : GroovyElementVis registerFix(annotation, GrRemoveModifierFix(PsiModifier.DEFAULT, GroovyBundle.message("illegal.default.modifier.fix")), modifier) } } + + override fun visitLambdaExpression(expression: GrLambdaExpression) { + checkSingleArgumentLambda(expression) + super.visitLambdaExpression(expression) + } + + private fun checkSingleArgumentLambda(lambda: GrLambdaExpression) { + val parameterList = lambda.parameterList + parameterList.lParen?.let { return } + val parent = lambda.parent + when (parent) { + is GrAssignmentExpression, is GrVariable, is GrParenthesizedExpression -> return + is GrArgumentList -> if (parent.parent is GrMethodCallExpression) return + } + + holder.createErrorAnnotation(parameterList, GroovyBundle.message("illegal.single.argument.lambda")).apply { + registerFix(AddParenthesisToLambdaParameterAction(lambda)) + } + } } diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/AddParenthesisToLambdaParameterAction.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/AddParenthesisToLambdaParameterAction.kt new file mode 100644 index 000000000000..e7ce8e77dbfb --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/AddParenthesisToLambdaParameterAction.kt @@ -0,0 +1,47 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.groovy.annotator.intentions + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.SmartPsiElementPointer +import com.intellij.psi.util.createSmartPointer +import org.jetbrains.plugins.groovy.GroovyBundle.message +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory +import org.jetbrains.plugins.groovy.lang.psi.api.GrLambdaExpression + +class AddParenthesisToLambdaParameterAction(parameterList: GrLambdaExpression) : IntentionAction { + + private val myLambda: SmartPsiElementPointer = parameterList.createSmartPointer() + + override fun getText(): String = familyName + + override fun getFamilyName(): String = message("add.parenthesis.to.lambda.parameter.list") + + override fun startInWriteAction(): Boolean = true + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean { + val parameterList = myLambda.element?.parameterList ?: return false + parameterList.lParen ?: return true + return false + } + + override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { + val lambda = myLambda.element ?: return + val closureText = closureText(lambda) ?: return + val closure = GroovyPsiElementFactory.getInstance(project).createLambdaFromText(closureText) + lambda.replaceWithExpression(closure, false) + } + + private fun closureText(lambda: GrLambdaExpression): String? { + val closureText = StringBuilder() + closureText.append("(") + val parameterList = lambda.parameterList + appendTextBetween(closureText, parameterList.text, parameterList.lParen, parameterList.rParen) + closureText.append(")") + appendTextBetween(closureText, lambda.text, parameterList, null) + + return closureText.toString() + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/ConvertLambdaToClosureAction.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/ConvertLambdaToClosureAction.kt index b0f35dc53729..319fbee6367c 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/ConvertLambdaToClosureAction.kt +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/ConvertLambdaToClosureAction.kt @@ -4,7 +4,6 @@ package org.jetbrains.plugins.groovy.annotator.intentions import com.intellij.codeInsight.intention.IntentionAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project -import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.SmartPsiElementPointer import com.intellij.psi.util.createSmartPointer @@ -51,25 +50,4 @@ class ConvertLambdaToClosureAction(lambda: GrLambdaExpression) : IntentionAction closureText.append("}") return closureText.toString() } - - /** - * Appends [text] to the [builder] cutting length of [left] text from the start and length of [right] text from the end. - */ - private fun appendTextBetween(builder: StringBuilder, text: String, left: PsiElement?, right: PsiElement?) { - val start = left?.textLength ?: 0 - val end = text.length - (right?.textLength ?: 0) - builder.append(text, start, end) - } - - /** - * Appends text of elements to the [builder] between [start] and [stop]. - * If [stop] is `null` then all siblings of [start] are processed. - */ - private fun appendElements(builder: StringBuilder, start: PsiElement, stop: PsiElement) { - var current: PsiElement? = start.nextSibling - while (current !== null && current !== stop) { - builder.append(current.text) - current = current.nextSibling - } - } } diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/intentionUtil.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/intentionUtil.kt new file mode 100644 index 000000000000..81a82114ec01 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/intentions/intentionUtil.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.groovy.annotator.intentions + +import com.intellij.psi.PsiElement + +/** + * Appends [text] to the [builder] cutting length of [left] text from the start and length of [right] text from the end. + */ +internal fun appendTextBetween(builder: StringBuilder, text: String, left: PsiElement?, right: PsiElement?) { + val start = left?.textLength ?: 0 + val end = text.length - (right?.textLength ?: 0) + builder.append(text, start, end) +} + +/** + * Appends text of elements to the [builder] between [start] and [stop]. + * If [stop] is `null` then all siblings of [start] are processed. + */ +internal fun appendElements(builder: StringBuilder, start: PsiElement, stop: PsiElement) { + var current: PsiElement? = start.nextSibling + while (current !== null && current !== stop) { + builder.append(current.text) + current = current.nextSibling + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java index 335a247306fc..e9f55d4d291d 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/GroovyPsiElementFactory.java @@ -1,4 +1,4 @@ -// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.plugins.groovy.lang.psi; import com.intellij.openapi.components.ServiceManager; @@ -11,6 +11,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocComment; import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocMemberReference; import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocReferenceElement; +import org.jetbrains.plugins.groovy.lang.psi.api.GrLambdaExpression; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierList; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotationNameValuePair; @@ -187,6 +188,14 @@ public abstract class GroovyPsiElementFactory implements JVMElementFactory { return createClosureFromText(s, null); } + @NotNull + public abstract GrLambdaExpression createLambdaFromText(@NotNull String text, @Nullable PsiElement context); + + @NotNull + public GrLambdaExpression createLambdaFromText(@NotNull String text) { + return createLambdaFromText(text, null); + } + @NotNull public GrParameter createParameter(@NotNull String name, @Nullable String typeText, @Nullable GroovyPsiElement context) throws IncorrectOperationException { return createParameter(name, typeText, null, context); diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/GroovyPsiElementFactoryImpl.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/GroovyPsiElementFactoryImpl.java index 061262d1780c..85ce069d8c59 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/GroovyPsiElementFactoryImpl.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/GroovyPsiElementFactoryImpl.java @@ -1,4 +1,4 @@ -// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.plugins.groovy.lang.psi.impl; import com.intellij.lang.ASTNode; @@ -26,6 +26,7 @@ import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocMemberReference; import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocReferenceElement; import org.jetbrains.plugins.groovy.lang.groovydoc.psi.api.GrDocTag; import org.jetbrains.plugins.groovy.lang.psi.*; +import org.jetbrains.plugins.groovy.lang.psi.api.GrLambdaExpression; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifier; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierList; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation; @@ -361,6 +362,17 @@ public class GroovyPsiElementFactoryImpl extends GroovyPsiElementFactory { return ((GrClosableBlock)initializer); } + @NotNull + @Override + public GrLambdaExpression createLambdaFromText(@NotNull String lambdaText, PsiElement context) throws IncorrectOperationException { + GroovyFile psiFile = createGroovyFileChecked("def __hdsjfghkl_sdhjfshglk_foo = " + lambdaText, false, context); + final GrStatement st = psiFile.getStatements()[0]; + LOG.assertTrue(st instanceof GrVariableDeclaration, lambdaText); + final GrExpression initializer = ((GrVariableDeclaration)st).getVariables()[0].getInitializerGroovy(); + LOG.assertTrue(initializer instanceof GrLambdaExpression, lambdaText); + return ((GrLambdaExpression)initializer); + } + private GroovyFileImpl createDummyFile(@NotNull CharSequence text, boolean physical) { final String fileName = DUMMY_FILE_NAME + '.' + GroovyFileType.GROOVY_FILE_TYPE.getDefaultExtension(); final long stamp = System.currentTimeMillis(); diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/AddParenthesisToLambdaParameterTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/AddParenthesisToLambdaParameterTest.groovy new file mode 100644 index 000000000000..600da3d41271 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/AddParenthesisToLambdaParameterTest.groovy @@ -0,0 +1,41 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.groovy.lang.highlighting + +import groovy.transform.CompileStatic +import org.jetbrains.plugins.groovy.GroovyBundle +import org.jetbrains.plugins.groovy.util.ActionTest +import org.jetbrains.plugins.groovy.util.Groovy30Test +import org.junit.Test + +@CompileStatic +class AddParenthesisToLambdaParameterTest extends Groovy30Test implements ActionTest { + + private void doTest(String before, String after) { + doActionTest(GroovyBundle.message("add.parenthesis.to.lambda.parameter.list"), before, after) + } + + @Test + void 'statement expression'() { + doTest 'a -> /*mark*/ b', '(a) -> /*mark*/ b' + } + + @Test + void 'nested lambda'() { + doTest 'a -> b /*mark*/-> c', 'a -> (b) /*mark*/ -> c' + } + + @Test + void 'multyline'() { + doTest ''' +true ? aa -> { + //line comment + aa++ +} : () -> {} +''', ''' +true ? (aa) -> { + //line comment + aa++ +} : () -> {} +''' + } +} diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/Groovy30HighlightingTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/Groovy30HighlightingTest.groovy index 8521c3775b0d..a541537ab471 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/Groovy30HighlightingTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/Groovy30HighlightingTest.groovy @@ -76,4 +76,8 @@ I i = {3} void 'test reassigned var in lambda 2'() { highlightingTest GrUnresolvedAccessInspection } + + void 'test illegal single argument lambda'() { + highlightingTest () + } } diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/util/Groovy30Test.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/util/Groovy30Test.groovy new file mode 100644 index 000000000000..5dca9a92eaa2 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/util/Groovy30Test.groovy @@ -0,0 +1,15 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.plugins.groovy.util + +import com.intellij.testFramework.LightProjectDescriptor +import groovy.transform.CompileStatic +import org.jetbrains.plugins.groovy.GroovyProjectDescriptors + +@CompileStatic +abstract class Groovy30Test extends LightProjectTest { + + @Override + final LightProjectDescriptor getProjectDescriptor() { + GroovyProjectDescriptors.GROOVY_3_0 + } +} diff --git a/plugins/groovy/testdata/highlighting/v30/illegalSingleArgumentLambda.groovy b/plugins/groovy/testdata/highlighting/v30/illegalSingleArgumentLambda.groovy new file mode 100644 index 000000000000..de8dec2ebde4 --- /dev/null +++ b/plugins/groovy/testdata/highlighting/v30/illegalSingleArgumentLambda.groovy @@ -0,0 +1,7 @@ +a -> a +def l = b-> c-> d +def l2 = false ? e->e : f->f +if ((e->e) != null){} +m(g->g) +def l3 = h->h +l3 = j->j