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 d3f8bca78f6d..ffc53f89c5dd 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/SimplifyStreamApiCallChainsInspection.java @@ -16,11 +16,11 @@ package com.intellij.codeInspection; import com.intellij.codeInsight.FileModificationService; -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.JavaCodeStyleManager; +import com.intellij.psi.util.ClassUtil; import com.intellij.psi.util.PsiUtil; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nls; @@ -31,13 +31,16 @@ import org.jetbrains.annotations.Nullable; * @author Pavel.Dolgov */ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalInspectionTool { - private static final Logger LOG = Logger.getInstance("#" + SimplifyStreamApiCallChainsInspection.class.getName()); - private static final String FOR_EACH_METHOD = "forEach"; private static final String FOR_EACH_ORDERED_METHOD = "forEachOrdered"; private static final String STREAM_METHOD = "stream"; + private static final String EMPTY_METHOD = "empty"; private static final String AS_LIST_METHOD = "asList"; private static final String OF_METHOD = "of"; + private static final String EMPTY_LIST_METHOD = "emptyList"; + private static final String EMPTY_SET_METHOD = "emptySet"; + private static final String SINGLETON_LIST_METHOD = "singletonList"; + private static final String SINGLETON_METHOD = "singleton"; @Override public boolean isEnabledByDefault() { @@ -54,33 +57,55 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns return new JavaElementVisitor() { @Override public void visitMethodCallExpression(PsiMethodCallExpression methodCall) { - if (isCallOf(methodCall, CommonClassNames.JAVA_UTIL_COLLECTION, STREAM_METHOD, 0)) { + final PsiMethod method = methodCall.resolveMethod(); + if (isCallOf(method, CommonClassNames.JAVA_UTIL_COLLECTION, STREAM_METHOD, 0)) { final PsiMethodCallExpression qualifierCall = getQualifierMethodCall(methodCall); - if (isCallOf(qualifierCall, CommonClassNames.JAVA_UTIL_ARRAYS, AS_LIST_METHOD, 1)) { - final PsiExpression[] argumentExpressions = qualifierCall.getArgumentList().getExpressions(); - if (argumentExpressions.length == 1 && argumentExpressions[0].getType() instanceof PsiArrayType) { - holder.registerProblem(methodCall, null, "Arrays.asList().stream() can be replaced with Arrays.stream()", - new ArraysAsListSingleArrayFix()); + if (qualifierCall == null) return; + final PsiMethod qualifier = qualifierCall.resolveMethod(); + ReplaceCollectionStreamFix fix = null; + if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_ARRAYS, AS_LIST_METHOD, 1)) { + if (hasSingleArrayArgument(qualifierCall)) { + fix = new ArraysAsListSingleArrayFix(); } else { - holder.registerProblem(methodCall, null, "Arrays.asList().stream() can be replaced with Stream.of()", - new ArraysAsListVarargFix()); + fix = new ReplaceWithStreamOfFix("Arrays.asList()"); } } + else if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTIONS, SINGLETON_LIST_METHOD, 1)) { + if(!hasSingleArrayArgument(qualifierCall)) { + fix = new ReplaceSingletonWithStreamOfFix("Collections.singletonList()"); + } + } + else if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTIONS, SINGLETON_METHOD, 1)) { + if(!hasSingleArrayArgument(qualifierCall)) { + fix = new ReplaceSingletonWithStreamOfFix("Collections.singleton()"); + } + } + else if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTIONS, EMPTY_LIST_METHOD, 0)) { + fix = new ReplaceWithStreamEmptyFix(EMPTY_LIST_METHOD); + } + else if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTIONS, EMPTY_SET_METHOD, 0)) { + fix = new ReplaceWithStreamEmptyFix(EMPTY_SET_METHOD); + } + if (fix != null) { + holder.registerProblem(methodCall, null, fix.getMessage(), fix); + } } else { final String name; - if (isCallOf(methodCall, CommonClassNames.JAVA_UTIL_STREAM_STREAM, FOR_EACH_METHOD, 1)) { + if (isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_STREAM, FOR_EACH_METHOD, 1)) { name = FOR_EACH_METHOD; } - else if (isCallOf(methodCall, CommonClassNames.JAVA_UTIL_STREAM_STREAM, FOR_EACH_ORDERED_METHOD, 1)) { + else if (isCallOf(method, CommonClassNames.JAVA_UTIL_STREAM_STREAM, FOR_EACH_ORDERED_METHOD, 1)) { name = FOR_EACH_ORDERED_METHOD; } else { return; } final PsiMethodCallExpression qualifierCall = getQualifierMethodCall(methodCall); - if (isCallOf(qualifierCall, CommonClassNames.JAVA_UTIL_COLLECTION, STREAM_METHOD, 0)) { + if (qualifierCall == null) return; + final PsiMethod qualifier = qualifierCall.resolveMethod(); + if (isCallOf(qualifier, CommonClassNames.JAVA_UTIL_COLLECTION, STREAM_METHOD, 0)) { String message = "Collection.stream()." + name + "() can be replaced with Collection.forEach()"; final LocalQuickFix fix; if (FOR_EACH_METHOD.equals(name)) { @@ -97,6 +122,25 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns }; } + static boolean hasSingleArrayArgument(PsiMethodCallExpression qualifierCall) { + final PsiExpression[] argumentExpressions = qualifierCall.getArgumentList().getExpressions(); + if (argumentExpressions.length == 1) { + PsiType type = argumentExpressions[0].getType(); + if(type instanceof PsiArrayType) { + PsiType methodType = qualifierCall.getType(); + // Rule out cases like Arrays.asList(stringArr) + if(methodType instanceof PsiClassType) { + PsiType[] parameters = ((PsiClassType)methodType).getParameters(); + if(parameters.length == 1 && parameters[0].equals(type)) + return false; + } + return true; + } + } + return false; + } + + @Nullable private static PsiMethodCallExpression getQualifierMethodCall(PsiMethodCallExpression methodCall) { final PsiExpression qualifierExpression = methodCall.getMethodExpression().getQualifierExpression(); if (qualifierExpression instanceof PsiMethodCallExpression) { @@ -116,13 +160,12 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns } @Contract("null, _, _, _ -> false") - protected static boolean isCallOf(@Nullable PsiMethodCallExpression expression, + protected static boolean isCallOf(@Nullable PsiMethod method, @NotNull String className, @NotNull String methodName, int parametersCount) { - if (expression == null) return false; - final PsiMethod method = expression.resolveMethod(); - if (method != null && methodName.equals(method.getName()) && method.getParameterList().getParametersCount() == parametersCount) { + if (method == null) return false; + if (methodName.equals(method.getName()) && method.getParameterList().getParametersCount() == parametersCount) { final PsiClass containingClass = method.getContainingClass(); if (containingClass != null && className.equals(containingClass.getQualifiedName())) { return true; @@ -159,15 +202,34 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns @Nullable PsiExpression qualifierExpression); } - private static abstract class ArraysAsListFix extends CallChainFixBase { + private static abstract class ReplaceCollectionStreamFix extends CallChainFixBase { private final String myClassName; private final String myMethodName; + private final String myQualifierCall; - private ArraysAsListFix(String className, String methodName) { + private ReplaceCollectionStreamFix(String qualifierCall, String className, String methodName) { + myQualifierCall = qualifierCall; myClassName = className; myMethodName = methodName; } + String getMessage() { + return myQualifierCall + ".stream() can be replaced with " + ClassUtil.extractClassName(myClassName) + "." + myMethodName + "()"; + } + + @Nls + @NotNull + @Override + public String getName() { + return "Replace " + myQualifierCall + ".stream() with " + ClassUtil.extractClassName(myClassName) + "." + myMethodName + "()"; + } + + @Nullable + protected String getTypeParameter(@NotNull PsiMethodCallExpression qualifierCall) { + PsiType[] parameters = qualifierCall.getMethodExpression().getTypeParameters(); + return parameters.length == 1 ? parameters[0].getCanonicalText() : null; + } + @Override protected void replaceMethodCall(@NotNull PsiMethodCallExpression methodCall, @NotNull PsiMethodCallExpression qualifierCall, @@ -175,11 +237,12 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns methodCall.getArgumentList().replace(qualifierCall.getArgumentList()); final Project project = methodCall.getProject(); - PsiType[] parameters = qualifierCall.getMethodExpression().getTypeParameters(); + String typeParameter = getTypeParameter(qualifierCall); String replacement; - if(parameters.length == 1) { - replacement = myClassName + ".<" + parameters[0].getCanonicalText() + ">" + myMethodName; - } else { + if (typeParameter != null) { + replacement = myClassName + ".<" + typeParameter + ">" + myMethodName; + } + else { replacement = myClassName + "." + myMethodName; } final PsiExpression newMethodExpression = JavaPsiFacade.getElementFactory(project).createExpressionFromText(replacement, methodCall); @@ -187,22 +250,44 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns } } - private static class ArraysAsListVarargFix extends ArraysAsListFix { - private ArraysAsListVarargFix() { - super(CommonClassNames.JAVA_UTIL_STREAM_STREAM, OF_METHOD); + private static class ReplaceWithStreamOfFix extends ReplaceCollectionStreamFix { + private ReplaceWithStreamOfFix(String qualifierCall) { + super(qualifierCall, CommonClassNames.JAVA_UTIL_STREAM_STREAM, OF_METHOD); } @Nls @NotNull @Override public String getFamilyName() { - return "Replace Arrays.asList().stream() with Stream.of()"; + return "Replace with Stream.of()"; } } - private static class ArraysAsListSingleArrayFix extends ArraysAsListFix { + private static class ReplaceSingletonWithStreamOfFix extends ReplaceWithStreamOfFix { + private ReplaceSingletonWithStreamOfFix(String qualifierCall) { + super(qualifierCall); + } + + @Nullable + @Override + protected String getTypeParameter(@NotNull PsiMethodCallExpression qualifierCall) { + String typeParameter = super.getTypeParameter(qualifierCall); + if(typeParameter != null) + return typeParameter; + PsiType[] argTypes = qualifierCall.getArgumentList().getExpressionTypes(); + if(argTypes.length == 1) { + PsiType argType = argTypes[0]; + if(argType instanceof PsiArrayType) { + return argType.getCanonicalText(); + } + } + return null; + } + } + + private static class ArraysAsListSingleArrayFix extends ReplaceCollectionStreamFix { private ArraysAsListSingleArrayFix() { - super(CommonClassNames.JAVA_UTIL_ARRAYS, STREAM_METHOD); + super("Arrays.asList()", CommonClassNames.JAVA_UTIL_ARRAYS, STREAM_METHOD); } @Nls @@ -213,6 +298,19 @@ public class SimplifyStreamApiCallChainsInspection extends BaseJavaBatchLocalIns } } + private static class ReplaceWithStreamEmptyFix extends ReplaceCollectionStreamFix { + private ReplaceWithStreamEmptyFix(String qualifierMethodName) { + super("Collections." + qualifierMethodName + "()", CommonClassNames.JAVA_UTIL_STREAM_STREAM, EMPTY_METHOD); + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Replace with Stream.empty()"; + } + } + private static class CollectionForEachFix extends CallChainFixBase { @Nls @NotNull diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterArraysAsListSingleArrayElement.java b/java/java-tests/testData/inspection/streamApiCallChains/afterArraysAsListSingleArrayElement.java new file mode 100644 index 000000000000..d2e1879ff691 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterArraysAsListSingleArrayElement.java @@ -0,0 +1,10 @@ +// "Replace Arrays.asList().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +public class ArraysStreamSingleElementArray { + Stream stream(String[] args) { + return Stream.of(args); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionEmptyListStream.java b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionEmptyListStream.java new file mode 100644 index 000000000000..0844c35a3be3 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionEmptyListStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.emptyList().stream() with Stream.empty()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionEmptyListStream { + Stream stream(String[] args) { + return args.length == 1 ? Stream.empty() : Arrays.stream(args); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionEmptySetStream.java b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionEmptySetStream.java new file mode 100644 index 000000000000..0ecef4d847ae --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionEmptySetStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.emptySet().stream() with Stream.empty()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionEmptySetStream { + Stream stream(String[] args) { + return args.length == 1 ? Stream.empty() : Arrays.stream(args); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonArrayStream.java b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonArrayStream.java new file mode 100644 index 000000000000..e15445650d65 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonArrayStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.singleton().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionSingletonArrayStream { + Stream stream(String[] args) { + return Stream.of(args); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonListStream.java b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonListStream.java new file mode 100644 index 000000000000..5d9d095f3ab2 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonListStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.singletonList().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionSingletonListStream { + Stream stream(String[] args) { + return Stream.of("xyz"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonStream.java b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonStream.java new file mode 100644 index 000000000000..1296b9e8f5de --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/afterCollectionSingletonStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.singleton().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionSingletonStream { + Stream stream(String[] args) { + return Stream.of("xyz"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeArraysAsListSingleArrayElement.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeArraysAsListSingleArrayElement.java new file mode 100644 index 000000000000..39f21a5f4e36 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeArraysAsListSingleArrayElement.java @@ -0,0 +1,10 @@ +// "Replace Arrays.asList().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +public class ArraysStreamSingleElementArray { + Stream stream(String[] args) { + return Arrays.g[]>asList(args).stream(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionEmptyListStream.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionEmptyListStream.java new file mode 100644 index 000000000000..43fc33e45f87 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionEmptyListStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.emptyList().stream() with Stream.empty()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionEmptyListStream { + Stream stream(String[] args) { + return args.length == 1 ? Collections.emptyList().stream() : Arrays.stream(args); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionEmptySetStream.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionEmptySetStream.java new file mode 100644 index 000000000000..f081370cf6d9 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionEmptySetStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.emptySet().stream() with Stream.empty()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionEmptySetStream { + Stream stream(String[] args) { + return args.length == 1 ? Collections.emptySet().stream() : Arrays.stream(args); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonArrayStream.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonArrayStream.java new file mode 100644 index 000000000000..6dc7ae4ae321 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonArrayStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.singleton().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionSingletonArrayStream { + Stream stream(String[] args) { + return Collections.singleton(args).stream(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonListStream.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonListStream.java new file mode 100644 index 000000000000..496e736995e2 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonListStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.singletonList().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionSingletonListStream { + Stream stream(String[] args) { + return Collections.singletonList("xyz").stream(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonStream.java b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonStream.java new file mode 100644 index 000000000000..76739d228407 --- /dev/null +++ b/java/java-tests/testData/inspection/streamApiCallChains/beforeCollectionSingletonStream.java @@ -0,0 +1,10 @@ +// "Replace Collections.singleton().stream() with Stream.of()" "true" + +import java.util.*; +import java.util.stream.Stream; + +class CollectionSingletonStream { + Stream stream(String[] args) { + return Collections.singleton("xyz").stream(); + } +} \ No newline at end of file