IDEA-168967 Intention to extract local variable created in stream operation lambda to separate stream step.

This commit is contained in:
Tagir Valeev
2017-03-02 16:24:07 +07:00
parent 3e57c61729
commit ee38a5cfdd
34 changed files with 521 additions and 1 deletions

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.intention.impl;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.codeInspection.LambdaCanBeMethodReferenceInspection;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.util.LambdaRefactoringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.StreamApiUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import static com.intellij.util.ObjectUtils.tryCast;
/**
* @author Tagir Valeev
*/
public class ExtractStreamMapAction extends PsiElementBaseIntentionAction {
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
PsiLocalVariable variable =
PsiTreeUtil.getParentOfType(element, PsiLocalVariable.class, false, PsiStatement.class, PsiLambdaExpression.class);
if (!isApplicable(variable)) return false;
setText("Extract variable '" + Objects.requireNonNull(variable.getName()) + "' to separate stream step");
return true;
}
@Contract("null -> false")
private static boolean isApplicable(PsiLocalVariable variable) {
if (variable == null || variable.getName() == null) return false;
if (!StreamApiUtil.isSupportedStreamElement(variable.getType())) return false;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) return false;
PsiDeclarationStatement declaration = tryCast(variable.getParent(), PsiDeclarationStatement.class);
if (declaration == null || declaration.getDeclaredElements().length != 1) return false;
PsiCodeBlock block = tryCast(declaration.getParent(), PsiCodeBlock.class);
if (block == null) return false;
PsiLambdaExpression lambda = tryCast(block.getParent(), PsiLambdaExpression.class);
if (lambda == null) return false;
PsiParameterList parameters = lambda.getParameterList();
if (parameters.getParametersCount() != 1) return false;
PsiExpressionList args = tryCast(lambda.getParent(), PsiExpressionList.class);
if (args == null || args.getExpressions().length != 1) return false;
PsiMethodCallExpression call = tryCast(args.getParent(), PsiMethodCallExpression.class);
if (call == null ||
!InlineStreamMapAction.NEXT_METHODS.contains(call.getMethodExpression().getReferenceName()) ||
call.getMethodExpression().getQualifierExpression() == null) {
return false;
}
PsiMethod method = call.resolveMethod();
if (method == null ||
method.getParameterList().getParametersCount() != 1 ||
!InheritanceUtil.isInheritor(method.getContainingClass(), CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) {
return false;
}
PsiParameter parameter = parameters.getParameters()[0];
if (ExpressionUtils.isReferenceTo(initializer, parameter) && parameter.getType().equals(variable.getType())) {
// If conversion is applied in this case, then extracted previous step will be silently removed.
// While this is correct, it may confuse the user. Having "Local variable is redundant" warning with "inline" fix is enough here.
return false;
}
if (method.getName().startsWith("flatMap")) {
PsiType outType = StreamApiUtil.getStreamElementType(call.getType());
// flatMap from primitive type works only if the stream element type matches
if (variable.getType() instanceof PsiPrimitiveType && !variable.getType().equals(outType)) return false;
}
return ReferencesSearch.search(parameter).forEach(
(Processor<PsiReference>)ref -> PsiTreeUtil.isAncestor(initializer, ref.getElement(), false));
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
PsiLocalVariable variable =
PsiTreeUtil.getParentOfType(element, PsiLocalVariable.class, false, PsiStatement.class, PsiLambdaExpression.class);
if (variable == null) return;
String name = variable.getName();
if (name == null) return;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) return;
PsiLambdaExpression lambda = PsiTreeUtil.getParentOfType(variable, PsiLambdaExpression.class);
if (lambda == null) return;
PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(lambda, PsiMethodCallExpression.class);
if (call == null) return;
PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
if (qualifier == null) return;
String methodName = call.getMethodExpression().getReferenceName();
if (methodName == null) return;
PsiParameter parameter = ArrayUtil.getFirstElement(lambda.getParameterList().getParameters());
if (parameter == null) return;
PsiType outType = StreamApiUtil.getStreamElementType(call.getType(), false);
String mapOperation = StreamApiUtil.generateMapOperation(parameter, variable.getType(), initializer);
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
if (!mapOperation.isEmpty()) {
qualifier = (PsiExpression)qualifier.replace(factory.createExpressionFromText(qualifier.getText() + mapOperation, qualifier));
}
parameter = (PsiParameter)parameter.replace(factory.createParameter(variable.getName(), variable.getType(), parameter));
variable.delete();
LambdaRefactoringUtil.simplifyToExpressionLambda(lambda);
if (methodName.startsWith("map")) {
String replacement = StreamApiUtil.generateMapOperation(parameter, outType, lambda.getBody());
if (!replacement.isEmpty()) {
call = (PsiMethodCallExpression)call.replace(factory.createExpressionFromText(qualifier.getText() + replacement, call));
qualifier = call.getMethodExpression().getQualifierExpression();
}
else {
qualifier = (PsiExpression)call.replace(qualifier);
}
}
else {
PsiTypeElement typeElement = parameter.getTypeElement();
if (typeElement != null) {
if (methodName.startsWith("flatMap")) {
String targetName = "flatMap";
if (!(typeElement.getType() instanceof PsiPrimitiveType)) {
if (PsiType.INT.equals(outType)) {
targetName = "flatMapToInt";
}
else if (PsiType.LONG.equals(outType)) {
targetName = "flatMapToLong";
}
else if (PsiType.DOUBLE.equals(outType)) {
targetName = "flatMapToDouble";
}
}
ExpressionUtils.bindCallTo(call, targetName);
}
typeElement.delete();
}
}
if (qualifier instanceof PsiMethodCallExpression) {
PsiLambdaExpression newLambda =
tryCast(ArrayUtil.getFirstElement(((PsiMethodCallExpression)qualifier).getArgumentList().getExpressions()),
PsiLambdaExpression.class);
if (newLambda != null) {
LambdaCanBeMethodReferenceInspection.replaceLambdaWithMethodReference(newLambda);
}
}
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return "Extract to separate stream step";
}
}

View File

@@ -45,7 +45,7 @@ public class InlineStreamMapAction extends PsiElementBaseIntentionAction {
private static final Set<String> MAP_METHODS =
StreamEx.of("map", "mapToInt", "mapToLong", "mapToDouble", "mapToObj", "boxed", "asLongStream", "asDoubleStream").toSet();
private static final Set<String> NEXT_METHODS = StreamEx
static final Set<String> NEXT_METHODS = StreamEx
.of("flatMap", "flatMapToInt", "flatMapToLong", "flatMapToDouble", "forEach", "forEachOrdered", "anyMatch", "noneMatch", "allMatch")
.append(MAP_METHODS).toSet();

View File

@@ -0,0 +1,10 @@
// "Extract variable 'lowerCase' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
boolean test(List<String> list) {
return list.stream().map(String::toLowerCase)
.anyMatch(lowerCase -> "test".equals(lowerCase));
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'arr' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
String testArrayInitializer() {
return LongStream.of(1, 2, 3).mapToObj(x -> new long[]{x}).map(arr -> Arrays.toString(arr)).collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'l' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
long[] testAsLongStream(int[] x) {
return Arrays.stream(x).map(i -> i * 2).asLongStream().toArray();
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'l' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
Object[] testBoxed(int[] x) {
return Arrays.stream(x).asLongStream().boxed().toArray();
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'y' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
Stream.of("xyz").mapToInt(String::length).flatMap(y -> IntStream.range(0, y)).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'y' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
Stream.of("xyz").map(String::length).flatMapToInt(y -> IntStream.range(0, y)).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 's' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
IntStream.of(1, 2, 3).mapToObj(String::valueOf).flatMapToInt(s -> IntStream.range(0, s.length())).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'i' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void test2(List<String> list) {
list.stream().mapToInt(String::length).forEach(i -> System.out.println(i));
}
}

View File

@@ -0,0 +1,10 @@
// "Extract variable 'fn' to separate stream step" "true"
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Test {
void testFunction() {
Stream.of("a", "b", "c").<Supplier<String>>map(x -> x::trim).map(fn -> fn.get()).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'set' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testMap(List<Map<String, String>> list) {
list.stream().map(Map::keySet).flatMap(set -> set.stream()).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'y' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testRemoveMap() {
Stream.of("xyz").map(x -> x + x).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract variable 'l' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
long[] testMapRename(int[] x) {
return Arrays.stream(x).asLongStream().map(l -> l * l).toArray();
}
}

View File

@@ -0,0 +1,13 @@
// "Extract variable 'lowerCase' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
boolean test(List<String> list) {
return list.stream()
.anyMatch(s -> {
String <caret>lowerCase = s.toLowerCase();
return "test".equals(lowerCase);
});
}
}

View File

@@ -0,0 +1,13 @@
// "Extract variable 'lowerCase' to separate stream step" "false"
import java.util.*;
import java.util.stream.*;
public class Test {
boolean testWrong(List<String> list) {
return list.stream()
.anyMatch(s -> {
String <caret>lowerCase = s.toLowerCase();
return lowerCase.equals(s);
});
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'arr' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
String testArrayInitializer() {
return LongStream.of(1,2,3).mapToObj(x -> {
long[] <caret>arr = {x};
return Arrays.toString(arr);
}).collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'l' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
long[] testAsLongStream(int[] x) {
return Arrays.stream(x).mapToLong(i -> {
int <caret>l = i * 2;
return l;
}).toArray();
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'l' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
Object[] testBoxed(int[] x) {
return Arrays.stream(x).mapToObj(i -> {
long <caret>l = i;
return l;
}).toArray();
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'y' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
Stream.of("xyz").flatMapToInt(x -> {
int <caret>y = x.length();
return IntStream.range(0, y);
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,13 @@
// "Extract variable 'y' to separate stream step" "false"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
// no direct method which would flatMap from long to int type, so intention is disabled here
Stream.of("xyz").flatMapToInt(x -> {
long <caret>y = x.length();
return IntStream.range(0, (int) y);
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'y' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
Stream.of("xyz").flatMapToInt(x -> {
Integer <caret>y = x.length();
return IntStream.range(0, y);
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 's' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testFlatMap() {
IntStream.of(1, 2, 3).flatMap(x -> {
String <caret>s = String.valueOf(x);
return IntStream.range(0, s.length());
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'i' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void test2(List<String> list) {
list.stream().forEach(s -> {
int <caret>i = s.length();
System.out.println(i);
});
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'i' to separate stream step" "false"
import java.util.*;
import java.util.stream.*;
public class Test {
void test2(List<String> list) {
list.stream().forEach(s -> {
float <caret>i = s.length();
System.out.println(i);
});
}
}

View File

@@ -0,0 +1,13 @@
// "Extract variable 'fn' to separate stream step" "true"
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Test {
void testFunction() {
Stream.of("a", "b", "c").map(x -> {
Supplier<String> <caret>fn = x::trim;
return fn.get();
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'set' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testMap(List<Map<String, String>> list) {
list.stream().flatMap(map -> {
Set<String> <caret>set = map.keySet();
return set.stream();
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'y' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
void testRemoveMap() {
Stream.of("xyz").map(x -> {
String <caret>y = x+x;
return y;
}).forEach(System.out::println);
}
}

View File

@@ -0,0 +1,12 @@
// "Extract variable 'l' to separate stream step" "true"
import java.util.*;
import java.util.stream.*;
public class Test {
long[] testMapRename(int[] x) {
return Arrays.stream(x).mapToLong(i -> {
long <caret>l = i;
return l * l;
}).toArray();
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.intention;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
public class ExtractStreamMapActionTest extends LightIntentionActionTestCase {
public void test() throws Exception { doAllTests(); }
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/extractStreamMap";
}
}

View File

@@ -0,0 +1,7 @@
import java.util.List;
public class X {
boolean test(List<String> list) {
return list.stream()<spot>.map(String::toLowerCase)</spot>.anyMatch(lower -> lower.equals("test"));
}
}

View File

@@ -0,0 +1,10 @@
import java.util.List;
public class X {
boolean test(List<String> list) {
return list.stream().anyMatch(s -> {
<spot>String lower = s.toLowerCase();</spot>
return lower.equals("test");
});
}
}

View File

@@ -0,0 +1,5 @@
<html>
<body>
This intention extracts the variable declared inside lambda operation to the separate mapping step using Stream.map() or similar operation.
</body>
</html>

View File

@@ -938,6 +938,10 @@
<className>com.intellij.codeInsight.intention.impl.InlineStreamMapAction</className>
<category>Java/Streams</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.ExtractStreamMapAction</className>
<category>Java/Streams</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.ComposeFunctionChainAction</className>
<category>Java/Refactorings</category>