diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitConditionUtil.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitConditionUtil.java new file mode 100644 index 000000000000..3932547877f0 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitConditionUtil.java @@ -0,0 +1,72 @@ +/* + * 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.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; + +public class SplitConditionUtil { + public static PsiPolyadicExpression findCondition(PsiElement element) { + return findCondition(element, true, true); + } + + public static PsiPolyadicExpression findCondition(PsiElement element, boolean acceptAnd, boolean acceptOr) { + if (!(element instanceof PsiJavaToken)) { + return null; + } + PsiJavaToken token = (PsiJavaToken)element; + if (!(token.getParent() instanceof PsiPolyadicExpression)) return null; + + PsiPolyadicExpression expression = (PsiPolyadicExpression)token.getParent(); + boolean isAndExpression = acceptAnd && expression.getOperationTokenType() == JavaTokenType.ANDAND; + boolean isOrExpression = acceptOr && expression.getOperationTokenType() == JavaTokenType.OROR; + if (!isAndExpression && !isOrExpression) return null; + + while (expression.getParent() instanceof PsiPolyadicExpression) { + expression = (PsiPolyadicExpression)expression.getParent(); + if (isAndExpression && expression.getOperationTokenType() != JavaTokenType.ANDAND) return null; + if (isOrExpression && expression.getOperationTokenType() != JavaTokenType.OROR) return null; + } + return expression; + } + + public static PsiExpression getROperands(PsiPolyadicExpression expression, PsiJavaToken separator) throws IncorrectOperationException { + PsiElement next = PsiTreeUtil.skipSiblingsForward(separator, PsiWhiteSpace.class, PsiComment.class); + final int offsetInParent; + if (next == null) { + offsetInParent = separator.getStartOffsetInParent() + separator.getTextLength(); + } else { + offsetInParent = next.getStartOffsetInParent(); + } + + PsiElementFactory factory = JavaPsiFacade.getInstance(expression.getProject()).getElementFactory(); + String rOperands = expression.getText().substring(offsetInParent); + return factory.createExpressionFromText(rOperands, expression.getParent()); + } + + public static PsiExpression getLOperands(PsiPolyadicExpression expression, PsiJavaToken separator) throws IncorrectOperationException { + PsiElement prev = separator; + if (prev.getPrevSibling() instanceof PsiWhiteSpace) prev = prev.getPrevSibling(); + if (prev == null) { + throw new IncorrectOperationException("Unable to split '"+expression.getText()+"' left to '"+separator+"' (offset "+separator.getStartOffsetInParent()+")"); + } + + PsiElementFactory factory = JavaPsiFacade.getInstance(expression.getProject()).getElementFactory(); + String rOperands = expression.getText().substring(0, prev.getStartOffsetInParent()); + return factory.createExpressionFromText(rOperands, expression.getParent()); + } +} 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 new file mode 100644 index 000000000000..b388def97f1b --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitFilterAction.java @@ -0,0 +1,130 @@ +/* + * 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.psi.*; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +import static com.intellij.codeInsight.intention.impl.SplitConditionUtil.getLOperands; +import static com.intellij.codeInsight.intention.impl.SplitConditionUtil.getROperands; + +public class SplitFilterAction extends PsiElementBaseIntentionAction { + private static final Logger LOG = Logger.getInstance(SplitFilterAction.class.getName()); + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { + final PsiPolyadicExpression expression = SplitConditionUtil.findCondition(element, true, false); + if (expression == null || expression.getOperands().length < 2) return false; + + PsiElement parent = PsiUtil.skipParenthesizedExprUp(expression.getParent()); + if (!(parent instanceof PsiLambdaExpression)) return false; + if (((PsiLambdaExpression)parent).getParameterList().getParametersCount() != 1) return false; + parent = parent.getParent(); + + if (!(parent instanceof PsiExpressionList)) return false; + 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)) { + return true; + } + + return false; + } + + @NotNull + @Override + public String getText() { + return CodeInsightBundle.message("intention.split.filter.text"); + } + + @Override + @NotNull + public String getFamilyName() { + return CodeInsightBundle.message("intention.split.filter.family"); + } + + @Override + public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException { + try { + if (!FileModificationService.getInstance().preparePsiElementForWrite(element)) return; + + final PsiJavaToken token = (PsiJavaToken)element; + final PsiPolyadicExpression expression = SplitConditionUtil.findCondition(element, true, false); + + final PsiLambdaExpression lambdaExpression = PsiTreeUtil.getParentOfType(expression, PsiLambdaExpression.class); + LOG.assertTrue(lambdaExpression != null); + final String lambdaParameterName = lambdaExpression.getParameterList().getParameters()[0].getName(); + + final PsiMethodCallExpression methodCallExpression = PsiTreeUtil.getParentOfType(expression, PsiMethodCallExpression.class); + LOG.assertTrue(methodCallExpression != null, expression); + + PsiExpression lOperand = getLOperands(expression, token); + PsiExpression rOperand = getROperands(expression, token); + + final Collection comments = PsiTreeUtil.findChildrenOfType(expression, PsiComment.class); + + final PsiMethodCallExpression chainedCall = + (PsiMethodCallExpression)JavaPsiFacade.getElementFactory(project).createExpressionFromText("a.filter(" + lambdaParameterName + " -> x)", expression); + final PsiExpression argExpression = chainedCall.getArgumentList().getExpressions()[0]; + final PsiElement rReplaced = ((PsiLambdaExpression)argExpression).getBody().replace(rOperand); + + final PsiExpression compoundArg = methodCallExpression.getArgumentList().getExpressions()[0]; + + final int separatorOffset = token.getTextOffset(); + for (PsiComment comment : comments) { + if (comment.getTextOffset() < separatorOffset) { + compoundArg.getParent().add(comment); + } + else { + rReplaced.getParent().add(comment); + } + } + + ((PsiLambdaExpression)compoundArg).getBody().replace(lOperand); + + chainedCall.getMethodExpression().getQualifierExpression().replace(methodCallExpression); + methodCallExpression.replace(chainedCall); + } + catch (IncorrectOperationException e) { + LOG.error(e); + } + } + +} diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitIfAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitIfAction.java index f2a4c01e179e..a621440c1ee2 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitIfAction.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/SplitIfAction.java @@ -29,6 +29,9 @@ import com.intellij.refactoring.util.RefactoringUtil; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; +import static com.intellij.codeInsight.intention.impl.SplitConditionUtil.getLOperands; +import static com.intellij.codeInsight.intention.impl.SplitConditionUtil.getROperands; + /** * @author mike */ @@ -37,22 +40,8 @@ public class SplitIfAction extends PsiElementBaseIntentionAction { @Override public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { - if (!(element instanceof PsiJavaToken)) { - return false; - } - PsiJavaToken token = (PsiJavaToken)element; - if (!(token.getParent() instanceof PsiPolyadicExpression)) return false; - - PsiPolyadicExpression expression = (PsiPolyadicExpression)token.getParent(); - boolean isAndExpression = expression.getOperationTokenType() == JavaTokenType.ANDAND; - boolean isOrExpression = expression.getOperationTokenType() == JavaTokenType.OROR; - if (!isAndExpression && !isOrExpression) return false; - - while (expression.getParent() instanceof PsiPolyadicExpression) { - expression = (PsiPolyadicExpression)expression.getParent(); - if (isAndExpression && expression.getOperationTokenType() != JavaTokenType.ANDAND) return false; - if (isOrExpression && expression.getOperationTokenType() != JavaTokenType.OROR) return false; - } + final PsiPolyadicExpression expression = SplitConditionUtil.findCondition(element); + if (expression == null) return false; if (!(expression.getParent() instanceof PsiIfStatement)) return false; PsiIfStatement ifStatement = (PsiIfStatement)expression.getParent(); @@ -124,32 +113,6 @@ public class SplitIfAction extends PsiElementBaseIntentionAction { editor.getSelectionModel().removeSelection(); } - private static PsiExpression getROperands(PsiPolyadicExpression expression, PsiJavaToken separator) throws IncorrectOperationException { - PsiElement next = PsiTreeUtil.skipSiblingsForward(separator, PsiWhiteSpace.class, PsiComment.class); - final int offsetInParent; - if (next == null) { - offsetInParent = separator.getStartOffsetInParent() + separator.getTextLength(); - } else { - offsetInParent = next.getStartOffsetInParent(); - } - - PsiElementFactory factory = JavaPsiFacade.getInstance(expression.getProject()).getElementFactory(); - String rOperands = expression.getText().substring(offsetInParent); - return factory.createExpressionFromText(rOperands, expression.getParent()); - } - - private static PsiExpression getLOperands(PsiPolyadicExpression expression, PsiJavaToken separator) throws IncorrectOperationException { - PsiElement prev = separator; - if (prev.getPrevSibling() instanceof PsiWhiteSpace) prev = prev.getPrevSibling(); - if (prev == null) { - throw new IncorrectOperationException("Unable to split '"+expression.getText()+"' left to '"+separator+"' (offset "+separator.getStartOffsetInParent()+")"); - } - - PsiElementFactory factory = JavaPsiFacade.getInstance(expression.getProject()).getElementFactory(); - String rOperands = expression.getText().substring(0, prev.getStartOffsetInParent()); - return factory.createExpressionFromText(rOperands, expression.getParent()); - } - private static void doOrSplit(PsiIfStatement ifStatement, PsiPolyadicExpression expression, PsiJavaToken token, Editor editor) throws IncorrectOperationException { PsiExpression lOperand = getLOperands(expression, token); PsiExpression rOperand = getROperands(expression, token); diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/afterSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/afterSimple.java new file mode 100644 index 000000000000..8a940c96d95d --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/afterSimple.java @@ -0,0 +1,9 @@ +// "Split into filter's chain" "true" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A")//starts with A + ).filter(name -> name.length() > 1/*comment*/).findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeIncomplete.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeIncomplete.java new file mode 100644 index 000000000000..effb5bad674e --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/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") &&).findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeOr.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeOr.java new file mode 100644 index 000000000000..54263a51387c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeOr.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") || name.length() > 1).findAny(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeSimple.java new file mode 100644 index 000000000000..df6397a0bc2d --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter/beforeSimple.java @@ -0,0 +1,9 @@ +// "Split into filter's chain" "true" + +import java.util.stream.Stream; +class Test { + void foo(Stream stringStream ) { + stringStream.filter(name -> name.startsWith("A") //starts with A + && /*comment*/name.length() > 1).findAny(); + } +} diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/intention/SplitFilterActionTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/intention/SplitFilterActionTest.java new file mode 100644 index 000000000000..efe14b0a2055 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInsight/intention/SplitFilterActionTest.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 SplitFilterActionTest extends LightIntentionActionTestCase { + + public void test() throws Exception { doAllTests(); } + + @Override + protected String getBasePath() { + return "/codeInsight/daemonCodeAnalyzer/quickFix/splitFilter"; + } + + @Override + protected Sdk getProjectJDK() { + return IdeaTestUtil.getMockJdk18(); + } +} diff --git a/platform/core-api/src/com/intellij/psi/CommonClassNames.java b/platform/core-api/src/com/intellij/psi/CommonClassNames.java index 826016921fc0..c8fc3e29c239 100644 --- a/platform/core-api/src/com/intellij/psi/CommonClassNames.java +++ b/platform/core-api/src/com/intellij/psi/CommonClassNames.java @@ -103,6 +103,7 @@ public interface CommonClassNames { @NonNls String JAVA_UTIL_STREAM_BASE_STREAM = "java.util.stream.BaseStream"; @NonNls String JAVA_UTIL_STREAM_STREAM = "java.util.stream.Stream"; @NonNls String JAVA_UTIL_STREAM_COLLECTORS = "java.util.stream.Collectors"; + @NonNls String JAVA_UTIL_FUNCTION_PREDICATE = "java.util.function.Predicate"; @NonNls String JAVA_LANG_INVOKE_MH_POLYMORPHIC = "java.lang.invoke.MethodHandle.PolymorphicSignature"; diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties index 6255b4009dca..4faa6e2f1198 100644 --- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties +++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties @@ -173,6 +173,8 @@ intention.make.type.generic.family=Make Type Generic intention.make.type.generic.text=Change type of {0} to {1} 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.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/SplitFilterAction/after.java.template b/resources-en/src/intentionDescriptions/SplitFilterAction/after.java.template new file mode 100644 index 000000000000..de9168339fec --- /dev/null +++ b/resources-en/src/intentionDescriptions/SplitFilterAction/after.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/SplitFilterAction/before.java.template b/resources-en/src/intentionDescriptions/SplitFilterAction/before.java.template new file mode 100644 index 000000000000..48b51fa83347 --- /dev/null +++ b/resources-en/src/intentionDescriptions/SplitFilterAction/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") && name.length() > 1).findFirst(); + } +} \ No newline at end of file diff --git a/resources-en/src/intentionDescriptions/SplitFilterAction/description.html b/resources-en/src/intentionDescriptions/SplitFilterAction/description.html new file mode 100644 index 000000000000..700b41cdf697 --- /dev/null +++ b/resources-en/src/intentionDescriptions/SplitFilterAction/description.html @@ -0,0 +1,5 @@ + + +This intention converts stream.filter(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 a388ab0a7852..f5c86ceb5dab 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -765,6 +765,10 @@ com.intellij.codeInsight.intention.impl.SplitIfAction Java/Control Flow + + com.intellij.codeInsight.intention.impl.SplitFilterAction + Java/Streams + com.intellij.codeInsight.intention.impl.InvertIfConditionAction Java/Control Flow