diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 525ee571eeae..2bd2b99e1aa4 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -570,6 +570,11 @@ groupKey="group.names.declaration.redundancy" enabledByDefault="true" level="WARNING" implementationClass="com.intellij.codeInspection.RedundantStreamOptionalCallInspection" displayName="Redundant step in Stream or Optional call chain"/> + ALL_CONSUMING_OPERATIONS = new HashSet<>(Arrays.asList( + "sorted", + "count", + "reduce", + "max", + "min", + "sum", + "average", + "collect", + "toArray", + "forEach", + "summaryStatistics" + )); + + private static Set NON_LIMITING_OPERATIONS = new HashSet<>(Arrays.asList( + "filter", + "map", + "distinct", + "flatMap", + "flatMapToInt", + "flatMapToLong", + "flatMapToDouble", + "flatMapToObj", + "onClose", + "peek", + "skip", + "dropWhile", + "mapToDouble", + "mapToLong", + "mapToObj", + "mapToInt", + "parallel", + "boxed", + "sequential", + "unordered", + "asLongStream", + "asDoubleStream" + )); + + private static final CallMatcher INFINITE_SOURCE = CallMatcher.anyOf( + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM, "generate").parameterCount(1), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM, "iterate").parameterCount(2), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM, "generate").parameterCount(1), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM, "iterate").parameterCount(2), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM, "generate").parameterCount(1), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM, "iterate").parameterCount(2), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_STREAM, "generate").parameterCount(1), + CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_STREAM, "iterate").parameterCount(2), + CallMatcher.instanceCall("java.util.Random", "ints", "longs", "doubles").parameterCount(2), + CallMatcher.instanceCall("java.util.Random", "ints", "longs", "doubles").parameterCount(0) + ); + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) { + return PsiElementVisitor.EMPTY_VISITOR; + } + return new JavaElementVisitor() { + @Override + public void visitMethodCallExpression(PsiMethodCallExpression call) { + if (!INFINITE_SOURCE.test(call)) return; + PsiMethodCallExpression allConsumingCall = findSubsequentCall(call, + name -> ALL_CONSUMING_OPERATIONS.contains(name), + name -> NON_LIMITING_OPERATIONS.contains(name)); + if (allConsumingCall == null) return; + PsiElement nameElement = allConsumingCall.getMethodExpression().getReferenceNameElement(); + if (nameElement == null) return; + holder.registerProblem(nameElement, InspectionsBundle.message("inspection.endless.stream.description")); + } + }; + } +} diff --git a/java/java-impl/src/com/intellij/codeInspection/RedundantStreamOptionalCallInspection.java b/java/java-impl/src/com/intellij/codeInspection/RedundantStreamOptionalCallInspection.java index 803d16113773..6274efe08f65 100644 --- a/java/java-impl/src/com/intellij/codeInspection/RedundantStreamOptionalCallInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/RedundantStreamOptionalCallInspection.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.function.Predicate; import static com.intellij.util.ObjectUtils.tryCast; +import static com.siyeh.ig.psiutils.StreamApiUtil.findSubsequentCall; /** * @author Tagir Valeev @@ -150,23 +151,6 @@ public class RedundantStreamOptionalCallInspection extends AbstractBaseJavaLocal }; } - @Nullable - private static PsiMethodCallExpression findSubsequentCall(PsiMethodCallExpression call, - Predicate isWantedCall, - Predicate isAllowedIntermediateCall) { - for (PsiMethodCallExpression chainCall = ExpressionUtils.getCallForQualifier(call); chainCall != null; - chainCall = ExpressionUtils.getCallForQualifier(chainCall)) { - String name = chainCall.getMethodExpression().getReferenceName(); - if (name == null) return null; - if (isWantedCall.test(name)) return chainCall; - if (!isAllowedIntermediateCall.test(name) || - !InheritanceUtil.isInheritor(chainCall.getType(), CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) { - return null; - } - } - return null; - } - @NotNull static TextRange getRange(PsiMethodCallExpression call) { PsiReferenceExpression expression = call.getMethodExpression(); diff --git a/java/java-tests/testData/inspection/endlessStream/Collect.java b/java/java-tests/testData/inspection/endlessStream/Collect.java new file mode 100644 index 000000000000..cc927a3381da --- /dev/null +++ b/java/java-tests/testData/inspection/endlessStream/Collect.java @@ -0,0 +1,11 @@ +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +public class Collect { + public static void main(String[] args) { + new Random().ints().boxed().collect(Collectors.toList()); + new Random().doubles().boxed().collect(Collectors.toList()); + new Random().longs().boxed().collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/endlessStream/Limited.java b/java/java-tests/testData/inspection/endlessStream/Limited.java new file mode 100644 index 000000000000..8fbd3fe20d6c --- /dev/null +++ b/java/java-tests/testData/inspection/endlessStream/Limited.java @@ -0,0 +1,9 @@ +import java.util.stream.IntStream; + +public class Limited { + public static void main(String[] args) { + int sum1 = IntStream.iterate(0, i -> i + 1).limit(10).sum(); + int sum2 = IntStream.iterate(0, i -> i < 10, i -> i + 1).sum(); + System.out.println(IntStream.iterate(0, i -> i + 1).takeWhile(value -> value < 54).sorted().sum()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/endlessStream/Sorted.java b/java/java-tests/testData/inspection/endlessStream/Sorted.java new file mode 100644 index 000000000000..f1bf6f9ff7d1 --- /dev/null +++ b/java/java-tests/testData/inspection/endlessStream/Sorted.java @@ -0,0 +1,8 @@ +import java.util.stream.LongStream; + +public class Sorted { + public static void main(String[] args) { + long sum1 = LongStream.iterate(0, i -> i + 1).sorted().limit(10).sum(); + long sum2 = LongStream.generate(() -> 10).sorted().limit(10).sum(); + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/EndlessStreamInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/EndlessStreamInspectionTest.java new file mode 100644 index 000000000000..6bc485a62cf0 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/EndlessStreamInspectionTest.java @@ -0,0 +1,31 @@ +// Copyright 2000-2017 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 com.intellij.java.codeInspection; + +import com.intellij.JavaTestUtil; +import com.intellij.codeInspection.EndlessStreamInspection; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + + +public class EndlessStreamInspectionTest extends LightCodeInsightFixtureTestCase { + public void testCollect() {doTest();} + public void testSorted() {doTest();} + public void testLimited() {doTest();} + + @Override + protected String getTestDataPath() { + return JavaTestUtil.getJavaTestDataPath() + "/inspection/endlessStream"; + } + + @NotNull + @Override + protected LightProjectDescriptor getProjectDescriptor() { + return JAVA_9; + } + + private void doTest() { + myFixture.enableInspections(new EndlessStreamInspection()); + myFixture.testHighlighting(getTestName(false) + ".java"); + } +} \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/InspectionsBundle.properties b/platform/platform-resources-en/src/messages/InspectionsBundle.properties index af73962402ef..97d319235f08 100644 --- a/platform/platform-resources-en/src/messages/InspectionsBundle.properties +++ b/platform/platform-resources-en/src/messages/InspectionsBundle.properties @@ -933,4 +933,6 @@ inspection.undeclared.service.usage.message=Usage of service ''{0}'' is not decl inspection.move.condition.to.loop=Move condition to loop inspection.move.condition.to.loop.no.conversion.to.do.while=Don't suggest to replace to 'do while' -inspection.move.condition.to.loop.description=Conditional break inside infinite loop \ No newline at end of file +inspection.move.condition.to.loop.description=Conditional break inside infinite loop + +inspection.endless.stream.description=Non-short-circuit operation consumes the infinite stream \ No newline at end of file diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/StreamApiUtil.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/StreamApiUtil.java index ca11685d59ca..4d2d5831a127 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/StreamApiUtil.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/StreamApiUtil.java @@ -16,8 +16,14 @@ package com.siyeh.ig.psiutils; import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; import com.intellij.psi.util.PsiUtil; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Predicate; + +import static com.siyeh.ig.psiutils.ExpressionUtils.getCallForQualifier; /** * @author Tagir Valeev @@ -80,4 +86,29 @@ public class StreamApiUtil { } return true; } + + /** + * Returns call from call chain which name satisfies isWantedCall predicate. + * Also checks that all calls between start call and wanted call satisfies isAllowedIntermediateCall + * @param call call chain + * @param isWantedCall predicate on the name of wanted call + * @param isAllowedIntermediateCall predicate on the name of any other call between start call and wanted call + * @return call that satisfies isWantedCall predicate or null otherwise + */ + @Nullable + public static PsiMethodCallExpression findSubsequentCall(PsiMethodCallExpression call, + Predicate isWantedCall, + Predicate isAllowedIntermediateCall) { + for (PsiMethodCallExpression chainCall = getCallForQualifier(call); chainCall != null; + chainCall = getCallForQualifier(chainCall)) { + String name = chainCall.getMethodExpression().getReferenceName(); + if (name == null) return null; + if (isWantedCall.test(name)) return chainCall; + if (!isAllowedIntermediateCall.test(name) || + !InheritanceUtil.isInheritor(chainCall.getType(), CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) { + return null; + } + } + return null; + } } diff --git a/resources-en/src/inspectionDescriptions/EndlessStream.html b/resources-en/src/inspectionDescriptions/EndlessStream.html new file mode 100644 index 000000000000..18acf25f41e8 --- /dev/null +++ b/resources-en/src/inspectionDescriptions/EndlessStream.html @@ -0,0 +1,10 @@ + + +

Non-short-circuit operation consumes the infinite stream. Operation may be completed only by throwing the exception.

+ +

Example:

+

+ Stream.iterate(0, i -> i + 1).collect(Collectors.toList()) +

+ + \ No newline at end of file