diff --git a/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java b/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java index 1baaf5e17731..68f5f2ff61da 100644 --- a/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java +++ b/java/java-impl/src/com/intellij/codeInspection/streamMigration/CollectMigration.java @@ -25,8 +25,10 @@ import com.intellij.psi.search.searches.ReferencesSearch; import com.intellij.psi.util.InheritanceUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; +import com.intellij.util.ObjectUtils; import com.siyeh.ig.psiutils.EquivalenceChecker; import com.siyeh.ig.psiutils.ExpressionUtils; +import com.siyeh.ig.psiutils.MethodCallUtils; import com.siyeh.ig.psiutils.VariableAccessUtils; import one.util.streamex.EntryStream; import one.util.streamex.StreamEx; @@ -121,8 +123,8 @@ class CollectMigration extends BaseStreamApiMigration { PsiExpression qualifierExpression = methodExpression.getQualifierExpression(); if (tb.dependsOn(qualifierExpression)) return null; - List> extractors = - Arrays.asList(AddingTerminal::tryExtract, GroupingTerminal::tryExtract, ToMapTerminal::tryExtract); + List> extractors = Arrays + .asList(AddingTerminal::tryExtract, GroupingTerminal::tryExtract, ToMapTerminal::tryExtract, AddingAllTerminal::tryExtractAddAll); CollectTerminal terminal = StreamEx.of(extractors).map(extractor -> extractor.apply(tb, call)).nonNull().findFirst().orElse(null); if (terminal != null) { @@ -153,6 +155,15 @@ class CollectMigration extends BaseStreamApiMigration { } } + @Contract("null -> false") + static boolean isEmptyCollectionInitializer(PsiExpression expression) { + if (expression instanceof PsiNewExpression) { + PsiExpressionList argumentList = ((PsiNewExpression)expression).getArgumentList(); + return argumentList != null && argumentList.getExpressions().length == 0; + } + return false; + } + interface CollectTerminal { @Nullable default PsiElement getElementToReplace() { return null; } @@ -174,21 +185,18 @@ class CollectMigration extends BaseStreamApiMigration { } static class AddingTerminal implements CollectTerminal { - private @Nullable PsiVariable myTarget; - private final PsiType myTargetType; - private final PsiExpression myInitializer; - private final PsiVariable myElement; - private final PsiMethodCallExpression myAddCall; + @Nullable PsiVariable myTarget; + final PsiType myTargetType; + final PsiExpression myInitializer; + final PsiVariable myElement; + final PsiMethodCallExpression myAddCall; AddingTerminal(@NotNull PsiVariable target, PsiVariable element, PsiMethodCallExpression addCall) { this(target.getType(), target.getInitializer(), element, addCall); - if (myInitializer instanceof PsiNewExpression) { - final PsiExpressionList argumentList = ((PsiNewExpression)myInitializer).getArgumentList(); - if (argumentList != null && argumentList.getExpressions().length == 0) { - myTarget = target; - } + if (isEmptyCollectionInitializer(myInitializer)) { + myTarget = target; } } @@ -276,6 +284,42 @@ class CollectMigration extends BaseStreamApiMigration { } } + static class AddingAllTerminal extends AddingTerminal { + private final PsiMethodCallExpression myAddAllCall; + + AddingAllTerminal(PsiVariable target, PsiVariable element, PsiMethodCallExpression addAllCall) { + super(target, element, null); + myAddAllCall = addAllCall; + } + + @Override + public String generateIntermediate() { + PsiType[] typeParameters = myAddAllCall.getMethodExpression().getTypeParameters(); + String generic = ""; + if(typeParameters.length == 1) { + generic = "<"+typeParameters[0].getCanonicalText()+">"; + } + String method = MethodCallUtils.isVarArgCall(myAddAllCall) ? CommonClassNames.JAVA_UTIL_STREAM_STREAM + "." + generic + "of" + : CommonClassNames.JAVA_UTIL_ARRAYS + "." + generic + "stream"; + return ".flatMap(" + myElement.getName() + "->" + method + "(" + + StreamEx.of(myAddAllCall.getArgumentList().getExpressions()).skip(1).map(PsiExpression::getText).joining(",") + "))"; + } + + @Nullable + static AddingAllTerminal tryExtractAddAll(TerminalBlock tb, PsiMethodCallExpression call) { + if(!MethodCallUtils.isCallToStaticMethod(call, CommonClassNames.JAVA_UTIL_COLLECTIONS, "addAll", 2)) { + return null; + } + PsiExpression[] args = call.getArgumentList().getExpressions(); + if(args.length < 2) return null; + PsiReferenceExpression collectionReference = ObjectUtils.tryCast(args[0], PsiReferenceExpression.class); + if (collectionReference == null || tb.dependsOn(collectionReference)) return null; + PsiLocalVariable target = ObjectUtils.tryCast(collectionReference.resolve(), PsiLocalVariable.class); + if (target == null || StreamEx.of(args).skip(1).anyMatch(arg -> VariableAccessUtils.variableIsUsed(target, arg))) return null; + return new AddingAllTerminal(target, tb.getVariable(), call); + } + } + static class GroupingTerminal implements CollectTerminal { private final AddingTerminal myDownstream; private final PsiLocalVariable myTarget; @@ -335,11 +379,9 @@ class CollectMigration extends BaseStreamApiMigration { if (args.length != 2 || !(args[1] instanceof PsiLambdaExpression)) return null; PsiLambdaExpression lambda = (PsiLambdaExpression)args[1]; PsiExpression body = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody()); - if (!(body instanceof PsiNewExpression)) return null; - PsiExpressionList ctorArgs = ((PsiNewExpression)body).getArgumentList(); - if (ctorArgs != null && ctorArgs.getExpressions().length == 0) { + if (isEmptyCollectionInitializer(body)) { PsiLocalVariable variable = extractQualifierVariable(tb, qualifierCall); - if (variable != null && variable.getInitializer() instanceof PsiNewExpression) { + if (variable != null && isEmptyCollectionInitializer(variable.getInitializer())) { PsiType mapType = variable.getType(); PsiType valueType = PsiUtil.substituteTypeParameter(mapType, CommonClassNames.JAVA_UTIL_MAP, 1, false); if (valueType == null) return null; @@ -415,11 +457,7 @@ class CollectMigration extends BaseStreamApiMigration { return null; } PsiLocalVariable variable = extractQualifierVariable(tb, call); - if (variable == null) return null; - PsiExpression initializer = variable.getInitializer(); - if (!(initializer instanceof PsiNewExpression)) return null; - PsiExpressionList argumentList = ((PsiNewExpression)initializer).getArgumentList(); - if (argumentList == null || argumentList.getExpressions().length != 0) return null; + if (variable == null || !isEmptyCollectionInitializer(variable.getInitializer())) return null; return new ToMapTerminal(call, tb.getVariable(), variable); } } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/afterCollectionsAddAllArray.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/afterCollectionsAddAllArray.java new file mode 100644 index 000000000000..c048353e7b45 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/afterCollectionsAddAllArray.java @@ -0,0 +1,8 @@ +// "Replace with toArray" "true" +import java.util.*; + +public class Test { + Object[] test(List list) { + return list.stream().filter(Objects::nonNull).flatMap(Arrays::stream).sorted().toArray(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/afterCollectionsAddAllVarArg.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/afterCollectionsAddAllVarArg.java new file mode 100644 index 000000000000..cc1128a5aed5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/afterCollectionsAddAllVarArg.java @@ -0,0 +1,13 @@ +// "Replace with toArray" "true" +import java.util.*; +import java.util.stream.Stream; + +public class Test { + Object[] test(List list) { + return list.stream().filter(Objects::nonNull).flatMap(str -> Stream.of(str, str + str)).sorted().toArray(); + } + + public static void main(String[] args) { + System.out.println(Arrays.toString(new Test().test(Arrays.asList("a", "b", "ba", "x", null, "c")))); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/beforeCollectionsAddAllArray.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/beforeCollectionsAddAllArray.java new file mode 100644 index 000000000000..bceb3a32208a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/beforeCollectionsAddAllArray.java @@ -0,0 +1,15 @@ +// "Replace with toArray" "true" +import java.util.*; + +public class Test { + Object[] test(List list) { + List result = new LinkedList<>(); + for(String[] str : list) { + if(str != null) { + Collections.addAll(result, str); + } + } + result.sort(null); + return result.toArray(); + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/beforeCollectionsAddAllVarArg.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/beforeCollectionsAddAllVarArg.java new file mode 100644 index 000000000000..c7734d625b61 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamApiMigration/beforeCollectionsAddAllVarArg.java @@ -0,0 +1,19 @@ +// "Replace with toArray" "true" +import java.util.*; + +public class Test { + Object[] test(List list) { + List result = new LinkedList<>(); + for(String str : list) { + if(str != null) { + Collections.addAll(result, str, str+str); + } + } + result.sort(null); + return result.toArray(); + } + + public static void main(String[] args) { + System.out.println(Arrays.toString(new Test().test(Arrays.asList("a", "b", "ba", "x", null, "c")))); + } +} diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/MethodCallUtils.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/MethodCallUtils.java index 81bbcfa68f09..90f23ebf8eca 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/MethodCallUtils.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/MethodCallUtils.java @@ -15,6 +15,7 @@ */ package com.siyeh.ig.psiutils; +import com.intellij.codeInspection.dataFlow.instructions.MethodCallInstruction; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.MethodSignatureUtil; @@ -119,13 +120,15 @@ public class MethodCallUtils { public static boolean isCallToStaticMethod(@NotNull PsiMethodCallExpression expression, @NonNls @NotNull String calledOnClassName, @NonNls @NotNull String methodName, int parameterCount) { - if (!methodName.equals(getMethodName(expression)) || expression.getArgumentList().getExpressions().length != parameterCount) { + PsiExpression[] args = expression.getArgumentList().getExpressions(); + if (!methodName.equals(getMethodName(expression)) || args.length < parameterCount) { return false; } PsiMethod method = expression.resolveMethod(); if (method == null || - !method.getModifierList().hasExplicitModifier(PsiModifier.STATIC) || - method.getParameterList().getParametersCount() != parameterCount) { + !method.hasModifierProperty(PsiModifier.STATIC) || + method.getParameterList().getParametersCount() != parameterCount || + !method.isVarArgs() && args.length != parameterCount) { return false; } PsiClass aClass = method.getContainingClass(); @@ -137,6 +140,9 @@ public class MethodCallUtils { final PsiReferenceExpression methodExpression = expression.getMethodExpression(); if (methodNamePattern != null) { final String referenceName = methodExpression.getReferenceName(); + if (referenceName == null) { + return false; + } final Matcher matcher = methodNamePattern.matcher(referenceName); if (!matcher.matches()) { return false; @@ -289,6 +295,20 @@ public class MethodCallUtils { return targetMethod != null && MethodSignatureUtil.isSuperMethod(targetMethod, method); } + /** + * Returns true if given method call is a var-arg call + * + * @param call a call to test + * @return true if call is resolved to the var-arg method and var-arg form is actually used + */ + public static boolean isVarArgCall(PsiMethodCallExpression call) { + PsiMethod method = call.resolveMethod(); + if(method == null || !method.isVarArgs()) return false; + PsiSubstitutor substitutor = call.resolveMethodGenerics().getSubstitutor(); + return MethodCallInstruction + .isVarArgCall(method, substitutor, call.getArgumentList().getExpressions(), method.getParameterList().getParameters()); + } + public static boolean containsSuperMethodCall(@NotNull PsiMethod method) { final SuperCallVisitor visitor = new SuperCallVisitor(method); method.accept(visitor);