diff --git a/java/java-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java b/java/java-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java index d6bfcb984e31..ab7ebaedcf56 100644 --- a/java/java-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java @@ -32,6 +32,7 @@ import com.intellij.psi.util.InheritanceUtil; import com.intellij.refactoring.util.LambdaRefactoringUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.ObjectUtils; +import com.intellij.util.ThreeState; import com.siyeh.ig.psiutils.*; import one.util.streamex.StreamEx; import org.jetbrains.annotations.Contract; @@ -124,6 +125,7 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns "!" + psiClass.getName() + (argNegated ? ".anyMatch(x -> !(...))" : ".anyMatch(...)"), argNegated ? ALL_MATCH_METHOD : NONE_MATCH_METHOD)); } + handleBooleanIdentity(methodCall); } else if (isStreamCall(method, NONE_MATCH_METHOD, true)) { if(isParentNegated(methodCall)) { @@ -132,6 +134,7 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns if(isArgumentLambdaNegated(methodCall)) { registerMatchFix(methodCall, new SimplifyMatchNegationFix(psiClass.getName()+".noneMatch(x -> !(...))", ALL_MATCH_METHOD)); } + handleBooleanIdentity(methodCall); } else if (isStreamCall(method, ALL_MATCH_METHOD, true)) { if(isArgumentLambdaNegated(methodCall)) { @@ -140,6 +143,7 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns new SimplifyMatchNegationFix((parentNegated ? "!" : "") + psiClass.getName() + ".allMatch(x -> !(...))", parentNegated ? ANY_MATCH_METHOD : NONE_MATCH_METHOD)); } + handleBooleanIdentity(methodCall); } else { handleMapToObj(methodCall); @@ -148,6 +152,27 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns } } + private void handleBooleanIdentity(PsiMethodCallExpression call) { + PsiElement nameElement = call.getMethodExpression().getReferenceNameElement(); + if (nameElement == null) return; + PsiExpression[] args = call.getArgumentList().getExpressions(); + if (args.length != 1 || !isBooleanIdentity(args[0])) return; + PsiExpression qualifier = PsiUtil.skipParenthesizedExprDown(call.getMethodExpression().getQualifierExpression()); + if (!(qualifier instanceof PsiMethodCallExpression)) return; + PsiMethodCallExpression qualifierCall = (PsiMethodCallExpression)qualifier; + if (MethodCallUtils.isCallToMethod(qualifierCall, CommonClassNames.JAVA_UTIL_STREAM_STREAM, null, + "map", new PsiType[]{null})) { + PsiExpression[] qualifierArgs = qualifierCall.getArgumentList().getExpressions(); + if(qualifierArgs.length != 1) return; + PsiExpression qualifierArg = qualifierArgs[0]; + + if(canBePredicate(qualifierArg) != ThreeState.NO) { + holder.registerProblem(nameElement, "Can be merged with previous 'map' call", + new SimplifyCallChainFix(new RemoveBooleanIdentityFix())); + } + } + } + private void handleToArray(PsiMethodCallExpression methodCall) { if(isCollectionStream(getQualifierMethodCall(methodCall))) { PsiArrayType type = getArrayType(methodCall); @@ -346,6 +371,54 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns }; } + /** + * Returns yes if expression can be used as j.u.f.Predicate, no if cannot, + * unsure if can be used as Predicate after wrapping with (expression)::apply. + * + * @param expression expression to test + * @return yes, no or unsure + */ + @NotNull + private static ThreeState canBePredicate(PsiExpression expression) { + expression = PsiUtil.skipParenthesizedExprDown(expression); + if(expression instanceof PsiFunctionalExpression) return ThreeState.YES; + if(expression == null) return ThreeState.NO; + PsiType type = expression.getType(); + PsiType inType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_UTIL_FUNCTION_FUNCTION, 0, false); + if(inType == null) return ThreeState.NO; + Project project = expression.getProject(); + PsiClass predicateClass = + JavaPsiFacade.getInstance(project).findClass(CommonClassNames.JAVA_UTIL_FUNCTION_PREDICATE, expression.getResolveScope()); + if(predicateClass == null) return ThreeState.NO; + PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + PsiType wantedType = factory.createType(predicateClass, inType); + PsiExpression copy = factory.createExpressionFromText(expression.getText(), expression); + PsiType copyType = copy.getType(); + if(copyType != null && wantedType.isAssignableFrom(copyType)) return ThreeState.YES; + PsiMethodReferenceExpression methodRef = + (PsiMethodReferenceExpression)factory.createExpressionFromText("(" + expression.getText() + ")::apply", expression); + PsiType methodRefType = methodRef.getType(); + if(methodRefType != null && wantedType.isAssignableFrom(methodRefType)) return ThreeState.UNSURE; + return ThreeState.NO; + } + + private static boolean isBooleanIdentity(PsiExpression arg) { + arg = PsiUtil.skipParenthesizedExprDown(arg); + if (FunctionalExpressionUtils.isFunctionalReferenceTo(arg, CommonClassNames.JAVA_LANG_BOOLEAN, PsiType.BOOLEAN, + "booleanValue", PsiType.EMPTY_ARRAY) || + FunctionalExpressionUtils.isFunctionalReferenceTo(arg, CommonClassNames.JAVA_LANG_BOOLEAN, null, + "valueOf", PsiType.BOOLEAN)) { + return true; + } + if (arg instanceof PsiLambdaExpression) { + PsiLambdaExpression lambda = (PsiLambdaExpression)arg; + PsiExpression body = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody()); + PsiParameterList parameters = lambda.getParameterList(); + return parameters.getParametersCount() == 1 && ExpressionUtils.isReferenceTo(body, parameters.getParameters()[0]); + } + return false; + } + @Nullable private static ReplaceWithElementIterationFix findIndexedIterationFix(PsiMethodCallExpression methodCall) { PsiElement nameElement = methodCall.getMethodExpression().getReferenceNameElement(); @@ -1133,4 +1206,36 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns return CodeStyleManager.getInstance(project).reformat(result); } } + + private static class RemoveBooleanIdentityFix implements CallChainFix { + @Override + public String getName() { + return "Merge with previous 'map' call"; + } + + @Override + public void applyFix(@NotNull Project project, PsiElement element) { + PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class); + if (call == null) return; + PsiMethodCallExpression qualifier = ObjectUtils + .tryCast(PsiUtil.skipParenthesizedExprDown(call.getMethodExpression().getQualifierExpression()), PsiMethodCallExpression.class); + if (qualifier == null) return; + String name = call.getMethodExpression().getReferenceName(); + if (name == null) return; + PsiExpression[] args = qualifier.getArgumentList().getExpressions(); + if (args.length == 1) { + PsiExpression arg = args[0]; + PsiType argType = arg.getType(); + PsiMethod method = LambdaUtil.getFunctionalInterfaceMethod(argType); + if (canBePredicate(arg) == ThreeState.UNSURE && method != null) { + PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + String text = ParenthesesUtils.getText(arg, ParenthesesUtils.POSTFIX_PRECEDENCE) + "::" + method.getName(); + arg.replace(factory.createExpressionFromText(text, arg)); + } + } + qualifier.getMethodExpression().handleElementRename(name); + CommentTracker ct = new CommentTracker(); + ct.replaceAndRestoreComments(call, ct.markUnchanged(qualifier)); + } + } } diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanFn.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanFn.java new file mode 100644 index 000000000000..ddb871212397 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanFn.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn) { + return list.stream().allMatch(fn::apply); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanNestedTernaryFn.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanNestedTernaryFn.java new file mode 100644 index 000000000000..86b814b75fc0 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanNestedTernaryFn.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn, Function fn2, boolean b, boolean b2) { + return list.stream().allMatch((b ? fn : b2 ? fn2 : fn)::apply); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanTernaryFn.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanTernaryFn.java new file mode 100644 index 000000000000..e033aed702ef --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanTernaryFn.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn, Function fn2, boolean b) { + return list.stream().allMatch((b ? fn : fn2)::apply); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanTernaryMr.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanTernaryMr.java new file mode 100644 index 000000000000..7308fbf7afa5 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanTernaryMr.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, boolean b, boolean b2) { + return list.stream().allMatch(b ? String::isEmpty : b2 ? "foo"::equals : "bar"::equals); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanValueOf.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanValueOf.java new file mode 100644 index 000000000000..9bf08feb071a --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAllMatchBooleanValueOf.java @@ -0,0 +1,9 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; + +public class Test { + public boolean test(List list) { + return list.stream().allMatch(String::isEmpty); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchBooleanValue.java b/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchBooleanValue.java new file mode 100644 index 000000000000..a9f51b0d4c50 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterAnyMatchBooleanValue.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; + +public class Test { + public boolean test(List list) { + /* ditto boolean!*/ + return list.stream().anyMatch(String::isEmpty); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchDittoLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchDittoLambda.java new file mode 100644 index 000000000000..00c16f51fc78 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterNoneMatchDittoLambda.java @@ -0,0 +1,9 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; + +public class Test { + public boolean test(List list) { + return list.stream().noneMatch(String::isEmpty); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanFn.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanFn.java new file mode 100644 index 000000000000..305c0b802ad3 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanFn.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn) { + return list.stream().map(fn).allMatch(Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanNestedTernaryCustomType.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanNestedTernaryCustomType.java new file mode 100644 index 000000000000..096ae2e94110 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanNestedTernaryCustomType.java @@ -0,0 +1,16 @@ +// "Merge with previous 'map' call" "false" + +import java.util.List; +import java.util.function.Function; + +public class Test { + interface MyFunction extends Function {}; + + MyFunction fn3 = "xyz"::equals; + + public boolean test(List list, Function fn, Function fn2, boolean b, boolean b2) { + // neither "b ? fn : b2 ? fn2 : fn3" nor "(b ? fn : b2 ? fn2 : fn3)::apply" can serve as predicate + // all possible replacements are longer + return list.stream().map(b ? fn : b2 ? fn2 : fn3).allMatch(Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanNestedTernaryFn.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanNestedTernaryFn.java new file mode 100644 index 000000000000..f08692b0145e --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanNestedTernaryFn.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn, Function fn2, boolean b, boolean b2) { + return list.stream().map(b ? fn : b2 ? fn2 : fn).allMatch(Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryFn.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryFn.java new file mode 100644 index 000000000000..e0061ee59194 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryFn.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn, Function fn2, boolean b) { + return list.stream().map(b ? fn : fn2).allMatch(Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryFnMr.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryFnMr.java new file mode 100644 index 000000000000..2259a14ff804 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryFnMr.java @@ -0,0 +1,12 @@ +// "Merge with previous 'map' call" "false" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, Function fn, boolean b) { + // neither "b ? String::isEmpty : fn" nor "(b ? String::isEmpty : fn)::apply" can serve as predicate + // all possible replacements are longer + return list.stream().map(b ? String::isEmpty : fn).allMatch(Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryMr.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryMr.java new file mode 100644 index 000000000000..f3fccb00fd35 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanTernaryMr.java @@ -0,0 +1,10 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; +import java.util.function.Function; + +public class Test { + public boolean test(List list, boolean b, boolean b2) { + return list.stream().map(b ? String::isEmpty : b2 ? "foo"::equals : "bar"::equals).allMatch(Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanValueOf.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanValueOf.java new file mode 100644 index 000000000000..f6223b9dae3f --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAllMatchBooleanValueOf.java @@ -0,0 +1,11 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; + +public class Test { + public boolean test(List list) { + return list.stream().map(String::isEmpty).allMatch(b -> { + return Boolean.valueOf(b); + }); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchBooleanValue.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchBooleanValue.java new file mode 100644 index 000000000000..89cc40f7e429 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeAnyMatchBooleanValue.java @@ -0,0 +1,9 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; + +public class Test { + public boolean test(List list) { + return list.stream().map(String::isEmpty).anyMatch(/* ditto boolean!*/Boolean::booleanValue); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchDittoLambda.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchDittoLambda.java new file mode 100644 index 000000000000..ac03fa903e60 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeNoneMatchDittoLambda.java @@ -0,0 +1,9 @@ +// "Merge with previous 'map' call" "true" + +import java.util.List; + +public class Test { + public boolean test(List list) { + return list.stream().map(String::isEmpty).noneMatch(b -> b); + } +} \ No newline at end of file diff --git a/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html b/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html index 35f3175a11f2..642b67282add 100644 --- a/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html +++ b/resources-en/src/inspectionDescriptions/SimplifyStreamApiCallChains.html @@ -36,6 +36,9 @@ It allows to avoid creating redundant temporary objects when traversing a collec
  • stream.noneMatch(x -> !(...)) → stream.allMatch()
  • stream.allMatch(x -> !(...)) → stream.noneMatch()
  • !stream.allMatch(x -> !(...)) → stream.anyMatch()
  • +
  • stream.map().anyMatch(Boolean::booleanValue) -> stream.anyMatch()
  • +
  • stream.map().allMatch(Boolean::booleanValue) -> stream.allMatch()
  • +
  • stream.map().noneMatch(Boolean::booleanValue) -> stream.noneMatch()
  • Note that the replacements semantic may have minor difference in some cases.