From 94e3eb2c04a546ffbe98e103a49216deeba72f0e Mon Sep 17 00:00:00 2001 From: Anna Kozlova Date: Wed, 4 Nov 2015 15:48:36 +0100 Subject: [PATCH] compound filter from filters chain (IDEA-146147) --- .../impl/MergeFilterChainAction.java | 163 ++++++++++++++++++ .../intention/impl/SplitFilterAction.java | 14 +- .../quickFix/mergeFilter/afterComments.java | 11 ++ .../quickFix/mergeFilter/afterSimple.java | 8 + .../quickFix/mergeFilter/beforeComments.java | 10 ++ .../mergeFilter/beforeIncomplete.java | 8 + .../mergeFilter/beforeNotNeighbour.java | 8 + .../quickFix/mergeFilter/beforeSimple.java | 8 + .../intention/MergeFilterChainActionTest.java | 35 ++++ .../src/messages/CodeInsightBundle.properties | 2 + .../after.java.template | 9 + .../before.java.template | 8 + .../MergeFilterChainAction/description.html | 5 + .../SplitFilterAction/description.html | 2 +- resources/src/META-INF/IdeaPlugin.xml | 4 + 15 files changed, 281 insertions(+), 14 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/intention/impl/MergeFilterChainAction.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterComments.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterSimple.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeComments.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeIncomplete.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeNotNeighbour.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeSimple.java create mode 100644 java/java-tests/testSrc/com/intellij/codeInsight/intention/MergeFilterChainActionTest.java create mode 100644 resources-en/src/intentionDescriptions/MergeFilterChainAction/after.java.template create mode 100644 resources-en/src/intentionDescriptions/MergeFilterChainAction/before.java.template create mode 100644 resources-en/src/intentionDescriptions/MergeFilterChainAction/description.html diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/MergeFilterChainAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/MergeFilterChainAction.java new file mode 100644 index 000000000000..dc4238aad1b5 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/MergeFilterChainAction.java @@ -0,0 +1,163 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInsight.intention.impl; + +import com.intellij.codeInsight.CodeInsightBundle; +import com.intellij.codeInsight.FileModificationService; +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +public class MergeFilterChainAction extends PsiElementBaseIntentionAction { + private static final Logger LOG = Logger.getInstance(MergeFilterChainAction.class.getName()); + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull final PsiElement element) { + if (!(element instanceof PsiIdentifier)) return false; + final PsiElement parent = element.getParent(); + if (!(parent instanceof PsiReferenceExpression)) return false; + final PsiElement gParent = parent.getParent(); + if (!(gParent instanceof PsiMethodCallExpression)) return false; + + if (!isFilterCall((PsiMethodCallExpression)gParent)) return false; + + return getFilterToMerge((PsiMethodCallExpression)gParent) != null; + } + + private static PsiMethodCallExpression getFilterToMerge(PsiMethodCallExpression methodCallExpression) { + final PsiExpression qualifierExpression = methodCallExpression.getMethodExpression().getQualifierExpression(); + if (qualifierExpression instanceof PsiMethodCallExpression && isFilterCall((PsiMethodCallExpression)qualifierExpression)) { + return (PsiMethodCallExpression)qualifierExpression; + } + + final PsiElement parent = methodCallExpression.getParent(); + if (parent instanceof PsiReferenceExpression) { + final PsiElement gParent = parent.getParent(); + if (gParent instanceof PsiMethodCallExpression && isFilterCall((PsiMethodCallExpression)gParent)) { + return (PsiMethodCallExpression)gParent; + } + } + + return null; + } + + public static boolean isFilterCall(PsiMethodCallExpression methodCallExpression) { + if (!"filter".equals(methodCallExpression.getMethodExpression().getReferenceName())) return false; + + final PsiExpressionList argumentList = methodCallExpression.getArgumentList(); + final PsiExpression[] expressions = argumentList.getExpressions(); + if (expressions.length != 1) return false; + if (!(expressions[0] instanceof PsiLambdaExpression)) return false; + final PsiElement lambdaBody = ((PsiLambdaExpression)expressions[0]).getBody(); + if (!(lambdaBody instanceof PsiExpression)) return false; + + final PsiMethod method = methodCallExpression.resolveMethod(); + if (method == null) return false; + final PsiClass containingClass = method.getContainingClass(); + final PsiParameter[] parameters = method.getParameterList().getParameters(); + if (parameters.length == 1 && + InheritanceUtil.isInheritor(containingClass, false, CommonClassNames.JAVA_UTIL_STREAM_STREAM) && + InheritanceUtil.isInheritor(parameters[0].getType(), CommonClassNames.JAVA_UTIL_FUNCTION_PREDICATE)) { + return true; + } + + return false; + } + + @NotNull + @Override + public String getText() { + return CodeInsightBundle.message("intention.merge.filter.text"); + } + + @Override + @NotNull + public String getFamilyName() { + return CodeInsightBundle.message("intention.merge.filter.family"); + } + + @Override + public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException { + try { + if (!FileModificationService.getInstance().preparePsiElementForWrite(element)) return; + + final PsiMethodCallExpression filterCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class); + LOG.assertTrue(filterCall != null); + + final PsiMethodCallExpression filterToMerge = getFilterToMerge(filterCall); + LOG.assertTrue(filterToMerge != null); + + final PsiMethodCallExpression callToStay = filterCall.getTextLength() < filterToMerge.getTextLength() ? filterCall : filterToMerge; + final PsiMethodCallExpression callToEliminate = callToStay == filterCall ? filterToMerge : filterCall; + + final PsiLambdaExpression targetLambda = (PsiLambdaExpression)callToStay.getArgumentList().getExpressions()[0]; + final PsiParameter[] parameters = targetLambda.getParameterList().getParameters(); + final String name = parameters.length > 0 ? parameters[0].getName() : null; + + final PsiLambdaExpression sourceLambda = (PsiLambdaExpression)callToEliminate.getArgumentList().getExpressions()[0]; + if (name != null) { + final PsiParameter[] sourceLambdaParams = sourceLambda.getParameterList().getParameters(); + if (sourceLambdaParams.length > 0 && !name.equals(sourceLambdaParams[0].getName())) { + for (PsiReference reference : ReferencesSearch.search(sourceLambdaParams[0]).findAll()) { + final PsiElement referenceElement = reference.getElement(); + if (referenceElement instanceof PsiReferenceExpression) { + ((PsiReferenceExpression)referenceElement).handleElementRename(name); + } + } + } + } + + PsiElement targetBody = targetLambda.getBody(); + LOG.assertTrue(targetBody instanceof PsiExpression); + final PsiElement sourceLambdaBody = sourceLambda.getBody(); + + LOG.assertTrue(sourceLambdaBody instanceof PsiExpression); + + + final PsiExpression compoundExpression = JavaPsiFacade.getElementFactory(project) + .createExpressionFromText(targetBody.getText() + " && " + sourceLambdaBody.getText(), sourceLambda); + targetBody = targetBody.replace(compoundExpression); + CodeStyleManager.getInstance(project).reformat(targetBody); + + final PsiExpression qualifierExpression = callToEliminate.getMethodExpression().getQualifierExpression(); + LOG.assertTrue(qualifierExpression != null, callToEliminate); + final Collection comments = PsiTreeUtil.findChildrenOfType(callToEliminate, PsiComment.class); + for (PsiComment comment : comments) { + final TextRange commentRange = comment.getTextRange(); + if (!sourceLambdaBody.getTextRange().contains(commentRange) && + !qualifierExpression.getTextRange().contains(commentRange)) { + targetBody.add(comment); + } + } + callToEliminate.replace(qualifierExpression); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + +} diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitFilterAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitFilterAction.java index b388def97f1b..3b7a42c0fdd3 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitFilterAction.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitFilterAction.java @@ -22,7 +22,6 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.psi.*; -import com.intellij.psi.util.InheritanceUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; import com.intellij.util.IncorrectOperationException; @@ -50,18 +49,7 @@ public class SplitFilterAction extends PsiElementBaseIntentionAction { final PsiElement gParent = parent.getParent(); if (!(gParent instanceof PsiMethodCallExpression)) return false; - final PsiReferenceExpression methodExpression = ((PsiMethodCallExpression)gParent).getMethodExpression(); - if (!"filter".equals(methodExpression.getReferenceName())) return false; - final PsiExpressionList argumentList = ((PsiMethodCallExpression)gParent).getArgumentList(); - if (argumentList.getExpressions().length != 1) return false; - - final PsiMethod method = ((PsiMethodCallExpression)gParent).resolveMethod(); - if (method == null) return false; - final PsiClass containingClass = method.getContainingClass(); - final PsiParameter[] parameters = method.getParameterList().getParameters(); - if (parameters.length == 1 && - InheritanceUtil.isInheritor(containingClass, false, CommonClassNames.JAVA_UTIL_STREAM_STREAM) && - InheritanceUtil.isInheritor(parameters[0].getType(), CommonClassNames.JAVA_UTIL_FUNCTION_PREDICATE)) { + if (MergeFilterChainAction.isFilterCall((PsiMethodCallExpression)gParent)) { return true; } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterComments.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterComments.java new file mode 100644 index 000000000000..4b5df42c67f5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterComments.java @@ -0,0 +1,11 @@ +// "Merge filter's chain" "true" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A") && name.//comment2 + length() > 1//comment +/*comment1*/ + ).findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterSimple.java new file mode 100644 index 000000000000..f6b0c9dac854 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/afterSimple.java @@ -0,0 +1,8 @@ +// "Merge filter's chain" "true" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A") && name.length() > 1).findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeComments.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeComments.java new file mode 100644 index 000000000000..4861cc0dd09c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeComments.java @@ -0,0 +1,10 @@ +// "Merge filter's chain" "true" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A"))//comment + .filter(a -> a.//comment2 + length() > 1 /*comment1*/).findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeIncomplete.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeIncomplete.java new file mode 100644 index 000000000000..4d63f2ee14a0 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeIncomplete.java @@ -0,0 +1,8 @@ +// "Split into filter's chain" "false" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A")).filter().findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeNotNeighbour.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeNotNeighbour.java new file mode 100644 index 000000000000..67e1514346c9 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeNotNeighbour.java @@ -0,0 +1,8 @@ +// "Merge filter's chain" "false" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A")).map(a -> a).filter(b -> true); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeSimple.java new file mode 100644 index 000000000000..43feefa13e3f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter/beforeSimple.java @@ -0,0 +1,8 @@ +// "Merge filter's chain" "true" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A")).filter(a -> a.length() > 1).findAny(); + } +} diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/intention/MergeFilterChainActionTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/intention/MergeFilterChainActionTest.java new file mode 100644 index 000000000000..bcfbeb79b701 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInsight/intention/MergeFilterChainActionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInsight.intention; + +import com.intellij.codeInsight.daemon.LightIntentionActionTestCase; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.testFramework.IdeaTestUtil; + +public class MergeFilterChainActionTest extends LightIntentionActionTestCase { + + public void test() throws Exception { doAllTests(); } + + @Override + protected String getBasePath() { + return "/codeInsight/daemonCodeAnalyzer/quickFix/mergeFilter"; + } + + @Override + protected Sdk getProjectJDK() { + return IdeaTestUtil.getMockJdk18(); + } +} diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties index 4faa6e2f1198..0aadec9a3d6a 100644 --- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties +++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties @@ -175,6 +175,8 @@ intention.split.if.family=Split If intention.split.if.text=Split into 2 if's intention.split.filter.text=Split into filter's chain intention.split.filter.family=Split filter +intention.merge.filter.text=Merge filter's chain +intention.merge.filter.family=Merge filters intention.introduce.variable.text=Introduce local variable intention.encapsulate.field.text=Encapsulate field intention.implement.abstract.method.family=Implement Abstract Method diff --git a/resources-en/src/intentionDescriptions/MergeFilterChainAction/after.java.template b/resources-en/src/intentionDescriptions/MergeFilterChainAction/after.java.template new file mode 100644 index 000000000000..845b67285f61 --- /dev/null +++ b/resources-en/src/intentionDescriptions/MergeFilterChainAction/after.java.template @@ -0,0 +1,9 @@ + +import java.util.Optional; +import java.util.stream.Stream; + +public class X { + Optional foo(Stream stream) { + return stream.filter(name -> name.startsWith("A") && name.length() > 1).findFirst(); + } +} \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/MergeFilterChainAction/before.java.template b/resources-en/src/intentionDescriptions/MergeFilterChainAction/before.java.template new file mode 100644 index 000000000000..de9168339fec --- /dev/null +++ b/resources-en/src/intentionDescriptions/MergeFilterChainAction/before.java.template @@ -0,0 +1,8 @@ +import java.util.Optional; +import java.util.stream.Stream; + +public class X { + Optional foo(Stream stream) { + return stream.filter(name -> name.startsWith("A")).filter(name -> name.length() > 1).findFirst(); + } +} \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/MergeFilterChainAction/description.html b/resources-en/src/intentionDescriptions/MergeFilterChainAction/description.html new file mode 100644 index 000000000000..14a22448310c --- /dev/null +++ b/resources-en/src/intentionDescriptions/MergeFilterChainAction/description.html @@ -0,0 +1,5 @@ + + +This intention converts chain of Stream.filter calls into one filter call with conjunction condition. + + \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/SplitFilterAction/description.html b/resources-en/src/intentionDescriptions/SplitFilterAction/description.html index 700b41cdf697..0a9b8b4a9436 100644 --- a/resources-en/src/intentionDescriptions/SplitFilterAction/description.html +++ b/resources-en/src/intentionDescriptions/SplitFilterAction/description.html @@ -1,5 +1,5 @@ -This intention converts stream.filter(a && b) expression into 2 chained filter calls. +This intention converts stream.filter(s -> a && b) expression into 2 chained filter calls. diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index f5c86ceb5dab..8366db4fc4da 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -769,6 +769,10 @@ com.intellij.codeInsight.intention.impl.SplitFilterAction Java/Streams + + com.intellij.codeInsight.intention.impl.MergeFilterChainAction + Java/Streams + com.intellij.codeInsight.intention.impl.InvertIfConditionAction Java/Control Flow