IDEA-166896 Simplify stream chain in case of *match used with Boolean::booleanValue

This commit is contained in:
Tagir Valeev
2017-01-23 17:21:44 +07:00
parent ec72347510
commit 5cdeace236
18 changed files with 273 additions and 0 deletions
@@ -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));
}
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn) {
return list.stream().allMatch(fn::apply);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn, Function<String, Boolean> fn2, boolean b, boolean b2) {
return list.stream().allMatch((b ? fn : b2 ? fn2 : fn)::apply);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn, Function<String, Boolean> fn2, boolean b) {
return list.stream().allMatch((b ? fn : fn2)::apply);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, boolean b, boolean b2) {
return list.stream().allMatch(b ? String::isEmpty : b2 ? "foo"::equals : "bar"::equals);
}
}
@@ -0,0 +1,9 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
public class Test {
public boolean test(List<String> list) {
return list.stream().allMatch(String::isEmpty);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
public class Test {
public boolean test(List<String> list) {
/* ditto boolean!*/
return list.stream().anyMatch(String::isEmpty);
}
}
@@ -0,0 +1,9 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
public class Test {
public boolean test(List<String> list) {
return list.stream().noneMatch(String::isEmpty);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn) {
return list.stream().map(fn).all<caret>Match(Boolean::booleanValue);
}
}
@@ -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<Object, Boolean> {};
MyFunction fn3 = "xyz"::equals;
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn, Function<String, Boolean> 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).all<caret>Match(Boolean::booleanValue);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn, Function<String, Boolean> fn2, boolean b, boolean b2) {
return list.stream().map(b ? fn : b2 ? fn2 : fn).allM<caret>atch(Boolean::booleanValue);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> fn, Function<String, Boolean> fn2, boolean b) {
return list.stream().map(b ? fn : fn2).all<caret>Match(Boolean::booleanValue);
}
}
@@ -0,0 +1,12 @@
// "Merge with previous 'map' call" "false"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, Function<String, T> 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).all<caret>Match(Boolean::booleanValue);
}
}
@@ -0,0 +1,10 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
import java.util.function.Function;
public class Test {
public <T extends Boolean> boolean test(List<String> list, boolean b, boolean b2) {
return list.stream().map(b ? String::isEmpty : b2 ? "foo"::equals : "bar"::equals).allM<caret>atch(Boolean::booleanValue);
}
}
@@ -0,0 +1,11 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
public class Test {
public boolean test(List<String> list) {
return list.stream().map(String::isEmpty).allMa<caret>tch(b -> {
return Boolean.valueOf(b);
});
}
}
@@ -0,0 +1,9 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
public class Test {
public boolean test(List<String> list) {
return list.stream().map(String::isEmpty).a<caret>nyMatch(/* ditto boolean!*/Boolean::booleanValue);
}
}
@@ -0,0 +1,9 @@
// "Merge with previous 'map' call" "true"
import java.util.List;
public class Test {
public boolean test(List<String> list) {
return list.stream().map(String::isEmpty).none<caret>Match(b -> b);
}
}
@@ -36,6 +36,9 @@ It allows to avoid creating redundant temporary objects when traversing a collec
<li><code>stream.noneMatch(x -> !(...))</code> &rarr; <code>stream.allMatch()</code></li>
<li><code>stream.allMatch(x -> !(...))</code> &rarr; <code>stream.noneMatch()</code></li>
<li><code>!stream.allMatch(x -> !(...))</code> &rarr; <code>stream.anyMatch()</code></li>
<li><code>stream.map().anyMatch(Boolean::booleanValue)</code> -> <code>stream.anyMatch()</code></li>
<li><code>stream.map().allMatch(Boolean::booleanValue)</code> -> <code>stream.allMatch()</code></li>
<li><code>stream.map().noneMatch(Boolean::booleanValue)</code> -> <code>stream.noneMatch()</code></li>
</ul>
<p>
Note that the replacements semantic may have minor difference in some cases.