diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java b/java/java-analysis-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java index 5bc775d6751a..dcb6b192a3af 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java @@ -21,10 +21,8 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.psi.codeStyle.JavaCodeStyleManager; -import com.intellij.psi.util.ClassUtil; -import com.intellij.psi.util.PsiUtil; -import com.intellij.psi.util.RedundantCastUtil; -import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.psi.util.*; +import com.siyeh.ig.psiutils.BoolUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -55,6 +53,8 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns private static final String FIND_FIRST_METHOD = "findFirst"; private static final String FILTER_METHOD = "filter"; private static final String ANY_MATCH_METHOD = "anyMatch"; + private static final String NONE_MATCH_METHOD = "noneMatch"; + private static final String ALL_MATCH_METHOD = "allMatch"; private static final String COUNTING_COLLECTOR = "counting"; private static final String MIN_BY_COLLECTOR = "minBy"; @@ -90,11 +90,42 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns else if (isCallOf(method, CommonClassNames.JAVA_UTIL_OPTIONAL, IS_PRESENT_METHOD, 0)) { handleOptionalIsPresent(methodCall); } + else if (isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_STREAM, ANY_MATCH_METHOD, 1)) { + if(isParentNegated(methodCall)) { + boolean argNegated = isArgumentLambdaNegated(methodCall); + registerMatchFix(methodCall, + new SimplifyMatchNegationFix(argNegated ? "!Stream.anyMatch(x -> !(...))" : "!Stream.anyMatch(...)", + argNegated ? ALL_MATCH_METHOD : NONE_MATCH_METHOD)); + } + } + else if (isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_STREAM, NONE_MATCH_METHOD, 1)) { + if(isParentNegated(methodCall)) { + registerMatchFix(methodCall, new SimplifyMatchNegationFix("!Stream.noneMatch(...)", ANY_MATCH_METHOD)); + } + if(isArgumentLambdaNegated(methodCall)) { + registerMatchFix(methodCall, new SimplifyMatchNegationFix("Stream.noneMatch(x -> !(...))", ALL_MATCH_METHOD)); + } + } + else if (isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_STREAM, ALL_MATCH_METHOD, 1)) { + if(isArgumentLambdaNegated(methodCall)) { + boolean parentNegated = isParentNegated(methodCall); + registerMatchFix(methodCall, + new SimplifyMatchNegationFix(parentNegated ? "!Stream.allMatch(x -> !(...))" : "Stream.allMatch(x -> !(...))", + parentNegated ? ANY_MATCH_METHOD : NONE_MATCH_METHOD)); + } + } else { handleStreamForEach(methodCall, method); } } + void registerMatchFix(PsiMethodCallExpression methodCall, SimplifyMatchNegationFix fix) { + PsiElement nameElement = methodCall.getMethodExpression().getReferenceNameElement(); + if(nameElement != null) { + holder.registerProblem(nameElement, fix.getMessage(), new SimplifyCallChainFix(fix)); + } + } + private void handleOptionalIsPresent(PsiMethodCallExpression methodCall) { PsiExpression optionalQualifier = methodCall.getMethodExpression().getQualifierExpression(); if(optionalQualifier instanceof PsiMethodCallExpression) { @@ -212,6 +243,20 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns }; } + static boolean isParentNegated(PsiMethodCallExpression methodCall) { + PsiElement parent = PsiUtil.skipParenthesizedExprUp(methodCall.getParent()); + return parent instanceof PsiExpression && BoolUtils.isNegation((PsiExpression)parent); + } + + static boolean isArgumentLambdaNegated(PsiMethodCallExpression methodCall) { + PsiExpression[] expressions = methodCall.getArgumentList().getExpressions(); + if(expressions.length != 1) return false; + PsiExpression arg = expressions[0]; + if(!(arg instanceof PsiLambdaExpression)) return false; + PsiElement body = ((PsiLambdaExpression)arg).getBody(); + return body instanceof PsiExpression && BoolUtils.isNegation((PsiExpression)body); + } + static boolean hasSingleArrayArgument(PsiMethodCallExpression qualifierCall) { final PsiExpression[] argumentExpressions = qualifierCall.getArgumentList().getExpressions(); if (argumentExpressions.length == 1) { @@ -571,4 +616,65 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns return "Stream.filter()." + myFindMethodName + "().isPresent() can be replaced with Stream.anyMatch()"; } } + + private static class SimplifyMatchNegationFix implements CallChainFix { + private final String myFrom, myTo; + + private SimplifyMatchNegationFix(String from, String to) { + myFrom = from; + myTo = to; + } + + @Override + public String getName() { + return "Replace "+myFrom+" with Stream."+myTo+"(...)"; + } + + public String getMessage() { + return myFrom+" can be replaced with Stream."+myTo+"(...)"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement element = descriptor.getStartElement(); + if(element instanceof PsiIdentifier) { + String from = element.getText(); + boolean removeParentNegation; + boolean removeLambdaNegation; + switch(from) { + case ALL_MATCH_METHOD: + removeLambdaNegation = true; + removeParentNegation = myTo.equals(ANY_MATCH_METHOD); + break; + case ANY_MATCH_METHOD: + removeParentNegation = true; + removeLambdaNegation = myTo.equals(ALL_MATCH_METHOD); + break; + case NONE_MATCH_METHOD: + removeParentNegation = myTo.equals(ANY_MATCH_METHOD); + removeLambdaNegation = myTo.equals(ALL_MATCH_METHOD); + break; + default: + return; + } + PsiMethodCallExpression methodCall = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class); + if (methodCall == null) return; + if (removeParentNegation && !isParentNegated(methodCall)) return; + if (removeLambdaNegation && !isArgumentLambdaNegated(methodCall)) return; + if (!FileModificationService.getInstance().preparePsiElementForWrite(element.getContainingFile())) return; + PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + element.replace(factory.createIdentifier(myTo)); + if (removeLambdaNegation) { + // Casts and array bounds already checked in isArgumentLambdaNegated + PsiExpression body = (PsiExpression)((PsiLambdaExpression)methodCall.getArgumentList().getExpressions()[0]).getBody(); + PsiExpression negated = BoolUtils.getNegated(body); + LOG.assertTrue(negated != null); + body.replace(negated); + } + if (removeParentNegation) { + PsiUtil.skipParenthesizedExprUp(methodCall.getParent()).replace(methodCall); + } + } + } + } } diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchNegatedLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchNegatedLambda.java new file mode 100644 index 000000000000..62a5fb1f6d4f --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchNegatedLambda.java @@ -0,0 +1,10 @@ +// "Replace Stream.allMatch(x -> !(...)) with Stream.noneMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).noneMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchNegatedLambdaTwice.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchNegatedLambdaTwice.java new file mode 100644 index 000000000000..1b682b22e4a0 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchNegatedLambdaTwice.java @@ -0,0 +1,10 @@ +// "Replace !Stream.allMatch(x -> !(...)) with Stream.anyMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).anyMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchNegated.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchNegated.java new file mode 100644 index 000000000000..5d8217290911 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchNegated.java @@ -0,0 +1,10 @@ +// "Replace !Stream.anyMatch(...) with Stream.noneMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).noneMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchNegatedLambdaTwice.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchNegatedLambdaTwice.java new file mode 100644 index 000000000000..d51ced9ee88f --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchNegatedLambdaTwice.java @@ -0,0 +1,10 @@ +// "Replace !Stream.anyMatch(x -> !(...)) with Stream.allMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).allMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchNegated.java b/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchNegated.java new file mode 100644 index 000000000000..51b4c0b74601 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchNegated.java @@ -0,0 +1,10 @@ +// "Replace !Stream.noneMatch(...) with Stream.anyMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).anyMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchNegatedLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchNegatedLambda.java new file mode 100644 index 000000000000..e11a37fed210 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchNegatedLambda.java @@ -0,0 +1,10 @@ +// "Replace Stream.noneMatch(x -> !(...)) with Stream.allMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).allMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegated.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegated.java new file mode 100644 index 000000000000..df19ee9bf6fa --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegated.java @@ -0,0 +1,10 @@ +// "Replace Stream.allMatch(x -> !(...)) with Stream.noneMatch(...)" "false" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(!data.stream().flatMap(Collection::stream).allMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegatedLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegatedLambda.java new file mode 100644 index 000000000000..d6c2fe534a35 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegatedLambda.java @@ -0,0 +1,10 @@ +// "Replace Stream.allMatch(x -> !(...)) with Stream.noneMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).allMatch(str -> !str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegatedLambdaTwice.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegatedLambdaTwice.java new file mode 100644 index 000000000000..7a6514bd79c7 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchNegatedLambdaTwice.java @@ -0,0 +1,10 @@ +// "Replace !Stream.allMatch(x -> !(...)) with Stream.anyMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(!data.stream().flatMap(Collection::stream).allMatch(str -> !str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegated.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegated.java new file mode 100644 index 000000000000..204e69b28f30 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegated.java @@ -0,0 +1,10 @@ +// "Replace !Stream.anyMatch(...) with Stream.noneMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(!data.stream().flatMap(Collection::stream).anyMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegatedLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegatedLambda.java new file mode 100644 index 000000000000..32ce1a0d7618 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegatedLambda.java @@ -0,0 +1,10 @@ +// "Replace !Stream.anyMatch(x -> !(...)) with Stream.allMatch(...)" "false" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).anyMatch(str -> !str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegatedLambdaTwice.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegatedLambdaTwice.java new file mode 100644 index 000000000000..b2dfcd99b7ec --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchNegatedLambdaTwice.java @@ -0,0 +1,10 @@ +// "Replace !Stream.anyMatch(x -> !(...)) with Stream.allMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(!data.stream().flatMap(Collection::stream).anyMatch(str -> !str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchNegated.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchNegated.java new file mode 100644 index 000000000000..2b01f91af7ac --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchNegated.java @@ -0,0 +1,10 @@ +// "Replace !Stream.noneMatch(...) with Stream.anyMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(!data.stream().flatMap(Collection::stream).noneMatch(str -> str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchNegatedLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchNegatedLambda.java new file mode 100644 index 000000000000..a37b825badfe --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchNegatedLambda.java @@ -0,0 +1,10 @@ +// "Replace Stream.noneMatch(x -> !(...)) with Stream.allMatch(...)" "true" + +import java.util.*; + +class Test { + public boolean testAnyMatch(List> data) { + if(data.stream().flatMap(Collection::stream).noneMatch(str -> !str.isEmpty())) + return true; + } +} \ No newline at end of file diff --git a/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html b/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html index 5893d37ddfbe..6ae43bcaa8d0 100644 --- a/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html +++ b/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html @@ -23,6 +23,12 @@ It allows to avoid creating redundant temporary objects when traversing a collec
  • Stream.collect(Collectors.summingInt()) → Stream.mapToInt().sum()
  • Stream.collect(Collectors.summingLong()) → Stream.mapToLong().sum()
  • Stream.collect(Collectors.summingDouble()) → Stream.mapToDouble().sum()
  • +
  • !Stream.anyMatch() → Stream.noneMatch()
  • +
  • !Stream.anyMatch(x -> !(...)) → Stream.allMatch()
  • +
  • !Stream.noneMatch() → Stream.anyMatch()
  • +
  • Stream.noneMatch(x -> !(...)) → Stream.allMatch()
  • +
  • Stream.allMatch(x -> !(...)) → Stream.noneMatch()
  • +
  • !Stream.allMatch(x -> !(...)) → Stream.anyMatch()
  • Note that the replacements semantic may have minor difference in some cases.