IDEA-162662 inspection: simplify obvious stream collect transformation to direct java.util equivalent

This commit is contained in:
Tagir Valeev
2016-10-19 15:48:35 +07:00
parent 8c19d2e0bd
commit 57d38b610f
21 changed files with 384 additions and 24 deletions
@@ -20,7 +20,9 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.impl.PsiDiamondTypeUtil;
import com.intellij.psi.util.*;
import com.siyeh.ig.psiutils.BoolUtils;
import org.jetbrains.annotations.Contract;
@@ -30,6 +32,7 @@ import org.jetbrains.annotations.Nullable;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* @author Pavel.Dolgov
@@ -57,6 +60,9 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
private static final String ALL_MATCH_METHOD = "allMatch";
private static final String COUNTING_COLLECTOR = "counting";
private static final String TO_LIST_COLLECTOR = "toList";
private static final String TO_SET_COLLECTOR = "toSet";
private static final String TO_COLLECTION_COLLECTOR = "toCollection";
private static final String MIN_BY_COLLECTOR = "minBy";
private static final String MAX_BY_COLLECTOR = "maxBy";
private static final String MAPPING_COLLECTOR = "mapping";
@@ -150,6 +156,13 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
}
}
@Contract("null -> false")
private boolean isCollectionStream(PsiMethodCallExpression qualifierCall) {
if (qualifierCall == null) return false;
PsiMethod qualifier = qualifierCall.resolveMethod();
return isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTION, STREAM_METHOD, 0);
}
private void handleStreamForEach(PsiMethodCallExpression methodCall, PsiMethod method) {
final String name;
if (isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_STREAM, FOR_EACH_METHOD, 1)) {
@@ -162,9 +175,7 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
return;
}
final PsiMethodCallExpression qualifierCall = getQualifierMethodCall(methodCall);
if (qualifierCall == null) return;
final PsiMethod qualifier = qualifierCall.resolveMethod();
if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTION, STREAM_METHOD, 0)) {
if (isCollectionStream(qualifierCall)) {
final ReplaceStreamMethodFix fix = new ReplaceStreamMethodFix(name, FOR_EACH_METHOD, true);
holder
.registerProblem(methodCall, getCallChainRange(methodCall, qualifierCall), fix.getMessage(), new SimplifyCallChainFix(fix));
@@ -176,7 +187,7 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
if(parameter instanceof PsiMethodCallExpression) {
PsiMethodCallExpression collectorCall = (PsiMethodCallExpression)parameter;
PsiMethod collectorMethod = collectorCall.resolveMethod();
ReplaceCollectorFix fix = null;
ReplaceCollectorFix fix;
if(isCallOf(collectorMethod, CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, COUNTING_COLLECTOR, 0)) {
fix = new ReplaceCollectorFix(COUNTING_COLLECTOR, "count()", false);
} else if(isCallOf(collectorMethod, CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, MIN_BY_COLLECTOR, 1)) {
@@ -197,9 +208,26 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
fix = new ReplaceCollectorFix(SUMMING_LONG_COLLECTOR, "mapToLong({0}).sum()", false);
} else if(isCallOf(collectorMethod, CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, SUMMING_DOUBLE_COLLECTOR, 1)) {
fix = new ReplaceCollectorFix(SUMMING_DOUBLE_COLLECTOR, "mapToDouble({0}).sum()", false);
} else {
PsiType type = methodCall.getType();
if(type instanceof PsiClassType && !(((PsiClassType)type).resolve() instanceof PsiTypeParameter)) {
String replacement = collectorToCollection(collectorCall);
if (replacement != null) {
PsiMethodCallExpression qualifier = getQualifierMethodCall(methodCall);
if (isCollectionStream(qualifier)) {
PsiElement startElement = qualifier.getMethodExpression().getReferenceNameElement();
if (startElement != null) {
holder.registerProblem(methodCall, new TextRange(startElement.getTextOffset() - methodCall.getTextOffset(),
methodCall.getTextLength()),
"Can be replaced with '" + replacement + "' constructor",
new SimplifyCallChainFix(new SimplifyCollectionCreationFix(replacement)));
}
}
}
}
return;
}
if (fix != null &&
collectorCall.getArgumentList().getExpressions().length == collectorMethod.getParameterList().getParametersCount()) {
if (collectorCall.getArgumentList().getExpressions().length == collectorMethod.getParameterList().getParametersCount()) {
TextRange range = methodCall.getTextRange();
PsiElement nameElement = methodCall.getMethodExpression().getReferenceNameElement();
if(nameElement != null) {
@@ -247,6 +275,51 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
};
}
private static boolean isCollectionConstructor(PsiMethod ctor) {
if(!ctor.getModifierList().hasExplicitModifier(PsiModifier.PUBLIC)) return false;
PsiParameterList list = ctor.getParameterList();
if(list.getParametersCount() != 1) return false;
PsiParameter parameter = list.getParameters()[0];
PsiTypeElement typeElement = parameter.getTypeElement();
if(typeElement == null) return false;
PsiType type = typeElement.getType();
if(!(type instanceof PsiClassType)) return false;
PsiClass aClass = ((PsiClassType)type).resolve();
if(aClass == null) return false;
return CommonClassNames.JAVA_UTIL_COLLECTION.equals(aClass.getQualifiedName());
}
@Nullable
private static String collectorToCollection(PsiMethodCallExpression call) {
PsiMethod method = call.resolveMethod();
if(isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, TO_LIST_COLLECTOR, 0)) {
return CommonClassNames.JAVA_UTIL_ARRAY_LIST;
}
if(isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, TO_SET_COLLECTOR, 0)) {
return CommonClassNames.JAVA_UTIL_HASH_SET;
}
if(isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, TO_COLLECTION_COLLECTOR, 1)) {
PsiExpression[] expressions = call.getArgumentList().getExpressions();
if(expressions.length == 1 && expressions[0] instanceof PsiMethodReferenceExpression) {
PsiMethodReferenceExpression methodRef = (PsiMethodReferenceExpression)expressions[0];
if(methodRef.isConstructor()) {
PsiElement element = methodRef.resolve();
if(element instanceof PsiMethod) {
PsiMethod ctor = (PsiMethod)element;
if(ctor.getParameterList().getParametersCount() == 0) {
PsiClass aClass = ctor.getContainingClass();
if (aClass != null &&
Stream.of(aClass.getConstructors()).anyMatch(SimplifyStreamApiCallChainsInspection::isCollectionConstructor)) {
return aClass.getQualifiedName();
}
}
}
}
}
}
return null;
}
static boolean isParentNegated(PsiMethodCallExpression methodCall) {
PsiElement parent = PsiUtil.skipParenthesizedExprUp(methodCall.getParent());
return parent instanceof PsiExpression && BoolUtils.isNegation((PsiExpression)parent);
@@ -689,4 +762,51 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns
}
}
}
private static class SimplifyCollectionCreationFix implements CallChainFix {
private String myReplacement;
public SimplifyCollectionCreationFix(String replacement) {
myReplacement = replacement;
}
@Override
public String getName() {
return "Replace with '"+myReplacement+"' constructor";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement element = descriptor.getStartElement();
if(!(element instanceof PsiMethodCallExpression)) return;
PsiMethodCallExpression collectCall = (PsiMethodCallExpression)element;
PsiType type = collectCall.getType();
if(!(type instanceof PsiClassType)) return;
PsiClass resolvedType = ((PsiClassType)type).resolve();
if(resolvedType == null || resolvedType instanceof PsiTypeParameter) return;
PsiMethodCallExpression streamCall = getQualifierMethodCall(collectCall);
if(streamCall == null) return;
PsiExpression collectionExpression = streamCall.getMethodExpression().getQualifierExpression();
if(collectionExpression == null) return;
String typeText = type.getCanonicalText();
if(CommonClassNames.JAVA_UTIL_LIST.equals(resolvedType.getQualifiedName()) ||
CommonClassNames.JAVA_UTIL_SET.equals(resolvedType.getQualifiedName())) {
PsiType[] parameters = ((PsiClassType)type).getParameters();
if(parameters.length != 1) return;
typeText = myReplacement + "<" + parameters[0].getCanonicalText() + ">";
}
if (!FileModificationService.getInstance().preparePsiElementForWrite(element)) return;
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
PsiExpression result = factory
.createExpressionFromText("new " + typeText + "(" + collectionExpression.getText() + ")", element);
PsiNewExpression newExpression = (PsiNewExpression)element.replace(result);
PsiJavaCodeReferenceElement classReference = newExpression.getClassOrAnonymousClassReference();
LOG.assertTrue(classReference != null);
JavaCodeStyleManager.getInstance(project).shortenClassReferences(classReference);
if (PsiDiamondTypeUtil.canCollapseToDiamond(newExpression, newExpression, null)) {
PsiDiamondTypeUtil.replaceExplicitWithDiamond(classReference.getParameterList());
}
CodeStyleManager.getInstance(project).reformat(newExpression);
}
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
new TreeSet<>(s).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
public static <T, T1 extends T> void test(List<T1> s) {
new TreeSet<T>(s).contains("abc");
}
}
@@ -0,0 +1,18 @@
// "Replace with 'Test.MyType' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
static class MyType extends ArrayList<String> {
public MyType() {}
public MyType(Collection<String> coll) {
super(coll);
}
}
public static void test(List<String> s) {
new MyType(s).contains("abc");
}
}
@@ -0,0 +1,18 @@
// "Replace with 'Test.MyType' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
static class MyType<A,B> extends ArrayList<String> {
public MyType() {}
public MyType(Collection<String> coll) {
super(coll);
}
}
public static void testMy(List<String> s) {
new MyType<String, Number>(s).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
new TreeSet<Object>(s).contains("abc");
}
}
@@ -0,0 +1,11 @@
// "Replace with 'java.util.ArrayList' constructor" "true"
import java.util.ArrayList;
import java.util.List;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
new ArrayList<>(s).contains("abc");
}
}
@@ -0,0 +1,11 @@
// "Replace with 'java.util.ArrayList' constructor" "true"
import java.util.ArrayList;
import java.util.List;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
new ArrayList<Object>(s).contains("abc");
}
}
@@ -0,0 +1,11 @@
// "Replace with 'java.util.HashSet' constructor" "true"
import java.util.HashSet;
import java.util.List;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
new HashSet<>(s).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
s.str<caret>eam().collect(Collectors.toCollection(TreeSet<String>::new)).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
public static <T, T1 extends T> void test(List<T1> s) {
s.stream().colle<caret>ct(Collectors.toCollection(TreeSet<T>::new)).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "false"
import java.util.*;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
s.str<caret>eam().collect(Collectors.toCollection(TreeSet<? extends String>::new)).contains("abc");
}
}
@@ -0,0 +1,14 @@
// "Replace with 'Test.MyType' constructor" "false"
import java.util.*;
import java.util.stream.*;
class Test {
static class MyType extends ArrayList<String> {
}
public static void test(List<String> s) {
s.str<caret>eam().collect(Collectors.toCollection(MyType::new)).contains("abc");
}
}
@@ -0,0 +1,18 @@
// "Replace with 'Test.MyType' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
static class MyType extends ArrayList<String> {
public MyType() {}
public MyType(Collection<String> coll) {
super(coll);
}
}
public static void test(List<String> s) {
s.str<caret>eam().collect(Collectors.toCollection(MyType::new)).contains("abc");
}
}
@@ -0,0 +1,18 @@
// "Replace with 'Test.MyType' constructor" "false"
import java.util.*;
import java.util.stream.*;
class Test {
static class MyType extends ArrayList<String> {
public MyType() {}
private MyType(Collection<String> coll) {
super(coll);
}
}
public static void test(List<String> s) {
s.str<caret>eam().collect(Collectors.toCollection(MyType::new)).contains("abc");
}
}
@@ -0,0 +1,18 @@
// "Replace with 'Test.MyType' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
static class MyType<A,B> extends ArrayList<String> {
public MyType() {}
public MyType(Collection<String> coll) {
super(coll);
}
}
public static void testMy(List<String> s) {
s.stream().collect(Collectors.toCollection(MyType<caret><String, Number>::new)).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.TreeSet' constructor" "true"
import java.util.*;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
s.str<caret>eam().collect(Collectors.toCollection(TreeSet<Object>::new)).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.ArrayList' constructor" "true"
import java.util.List;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
s.stream().collect(Collectors.toL<caret>ist()).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.ArrayList' constructor" "true"
import java.util.List;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
s.stream().collect(Collectors.<Object>toL<caret>ist()).contains("abc");
}
}
@@ -0,0 +1,10 @@
// "Replace with 'java.util.HashSet' constructor" "true"
import java.util.List;
import java.util.stream.*;
class Test {
public static void test(List<String> s) {
s.stream().co<caret>llect(Collectors.toSet()).contains("abc");
}
}
@@ -6,29 +6,32 @@ It allows to avoid creating redundant temporary objects when traversing a collec
The following call chains are replaced by this inspection:
</p>
<ul>
<li><code>Collection.stream().forEach()</code> &rarr; <code>Collection.forEach()</code></li>
<li><code>Collection.stream().forEachOrdered()</code> &rarr; <code>Collection.forEach()</code></li>
<li><code>collection.stream().forEach()</code> &rarr; <code>collection.forEach()</code></li>
<li><code>collection.stream().forEachOrdered()</code> &rarr; <code>collection.forEach()</code></li>
<li><code>collection.stream().collect(Collectors.toList())</code> &rarr; <code>new ArrayList&lt;&gt;(collection)</code></li>
<li><code>collection.stream().collect(Collectors.toSet())</code> &rarr; <code>new HashSet&lt;&gt;(collection)</code></li>
<li><code>collection.stream().collect(Collectors.toCollection(CollectionType::new))</code> &rarr; <code>new CollectionType&lt;&gt;(collection)</code></li>
<li><code>Arrays.asList().stream()</code> &rarr; <code>Arrays.stream()</code> or <code>Stream.of()</code></li>
<li><code>Collections.singleton().stream()</code> &rarr; <code>Stream.of()</code></li>
<li><code>Collections.singletonList().stream()</code> &rarr; <code>Stream.of()</code></li>
<li><code>Collections.emptyList().stream()</code> &rarr; <code>Stream.empty()</code></li>
<li><code>Collections.emptySet().stream()</code> &rarr; <code>Stream.empty()</code></li>
<li><code>Stream.filter().findFirst().isPresent()</code> &rarr; <code>Stream.anyMatch()</code></li>
<li><code>Stream.filter().findAny().isPresent()</code> &rarr; <code>Stream.anyMatch()</code></li>
<li><code>Stream.collect(Collectors.counting())</code> &rarr; <code>Stream.count()</code></li>
<li><code>Stream.collect(Collectors.maxBy())</code> &rarr; <code>Stream.max()</code></li>
<li><code>Stream.collect(Collectors.minBy())</code> &rarr; <code>Stream.min()</code></li>
<li><code>Stream.collect(Collectors.mapping())</code> &rarr; <code>Stream.map().collect()</code></li>
<li><code>Stream.collect(Collectors.reducing())</code> &rarr; <code>Stream.reduce()</code> or <code>Stream.map().reduce()</code></li>
<li><code>Stream.collect(Collectors.summingInt())</code> &rarr; <code>Stream.mapToInt().sum()</code></li>
<li><code>Stream.collect(Collectors.summingLong())</code> &rarr; <code>Stream.mapToLong().sum()</code></li>
<li><code>Stream.collect(Collectors.summingDouble())</code> &rarr; <code>Stream.mapToDouble().sum()</code></li>
<li><code>!Stream.anyMatch()</code> &rarr; <code>Stream.noneMatch()</code></li>
<li><code>!Stream.anyMatch(x -> !(...))</code> &rarr; <code>Stream.allMatch()</code></li>
<li><code>!Stream.noneMatch()</code> &rarr; <code>Stream.anyMatch()</code></li>
<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.filter().findFirst().isPresent()</code> &rarr; <code>stream.anyMatch()</code></li>
<li><code>stream.filter().findAny().isPresent()</code> &rarr; <code>stream.anyMatch()</code></li>
<li><code>stream.collect(Collectors.counting())</code> &rarr; <code>stream.count()</code></li>
<li><code>stream.collect(Collectors.maxBy())</code> &rarr; <code>stream.max()</code></li>
<li><code>stream.collect(Collectors.minBy())</code> &rarr; <code>stream.min()</code></li>
<li><code>stream.collect(Collectors.mapping())</code> &rarr; <code>stream.map().collect()</code></li>
<li><code>stream.collect(Collectors.reducing())</code> &rarr; <code>stream.reduce()</code> or <code>Stream.map().reduce()</code></li>
<li><code>stream.collect(Collectors.summingInt())</code> &rarr; <code>stream.mapToInt().sum()</code></li>
<li><code>stream.collect(Collectors.summingLong())</code> &rarr; <code>stream.mapToLong().sum()</code></li>
<li><code>stream.collect(Collectors.summingDouble())</code> &rarr; <code>stream.mapToDouble().sum()</code></li>
<li><code>!stream.anyMatch()</code> &rarr; <code>stream.noneMatch()</code></li>
<li><code>!stream.anyMatch(x -> !(...))</code> &rarr; <code>stream.allMatch()</code></li>
<li><code>!stream.noneMatch()</code> &rarr; <code>stream.anyMatch()</code></li>
<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>
</ul>
<p>
Note that the replacements semantic may have minor difference in some cases.