StreamToLoopInspection: support Map.forEach()

Fixes IDEA-190440 Convert usage of java.util.Map.forEach() to for (... entrySet()) loop
This commit is contained in:
Tagir Valeev
2018-04-19 12:12:40 +07:00
parent e8487f15df
commit c76d9aba62
6 changed files with 172 additions and 28 deletions
@@ -111,10 +111,16 @@ abstract class SourceOperation extends Operation {
}
static class ForEachSource extends SourceOperation {
private final boolean myEntrySet;
private @Nullable PsiExpression myQualifier;
ForEachSource(@Nullable PsiExpression qualifier) {
this(qualifier, false);
}
ForEachSource(@Nullable PsiExpression qualifier, boolean entrySet) {
myQualifier = qualifier;
myEntrySet = entrySet;
}
@Override
@@ -146,7 +152,8 @@ abstract class SourceOperation extends Operation {
public String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
PsiExpression iterationParameter = myQualifier == null ? ExpressionUtils
.getQualifierOrThis(((PsiMethodCallExpression)context.createExpression("stream()")).getMethodExpression()) : myQualifier;
return context.getLoopLabel() + "for(" + outVar.getDeclaration() + ": " + iterationParameter.getText() + ") {" + code + "}\n";
String iterationParameterText = iterationParameter.getText() + (myEntrySet ? ".entrySet()" : "");
return context.getLoopLabel() + "for(" + outVar.getDeclaration() + ": " + iterationParameterText + ") {" + code + "}\n";
}
}
@@ -28,6 +28,7 @@ import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.*;
import one.util.streamex.IntStreamEx;
import one.util.streamex.StreamEx;
@@ -52,6 +53,11 @@ public class StreamToLoopInspection extends AbstractBaseJavaLocalInspectionTool
"count", "sum", "summaryStatistics", "reduce", "collect", "findFirst", "findAny", "anyMatch", "allMatch", "noneMatch", "toArray",
"average", "forEach", "forEachOrdered", "min", "max", "toList", "toSet", "toImmutableList", "toImmutableSet");
private static final CallMatcher ITERABLE_FOREACH = CallMatcher.instanceCall(
CommonClassNames.JAVA_LANG_ITERABLE, "forEach").parameterTypes("java.util.function.Consumer");
private static final CallMatcher MAP_FOREACH = CallMatcher.instanceCall(
CommonClassNames.JAVA_UTIL_MAP, "forEach").parameterTypes("java.util.function.BiConsumer");
@SuppressWarnings("PublicField")
public boolean SUPPORT_UNKNOWN_SOURCES = false;
@@ -82,7 +88,7 @@ public class StreamToLoopInspection extends AbstractBaseJavaLocalInspectionTool
register(call, nameElement, "Replace Stream API chain with loop");
}
}
else if (extractIterableForEach(call) != null) {
else if (extractIterableForEach(call) != null || extractMapForEach(call) != null) {
register(call, nameElement, "Replace 'forEach' call with loop");
}
}
@@ -150,31 +156,59 @@ public class StreamToLoopInspection extends AbstractBaseJavaLocalInspectionTool
@Nullable
static List<OperationRecord> extractIterableForEach(PsiMethodCallExpression terminalCall) {
if (MethodCallUtils.isCallToMethod(terminalCall, CommonClassNames.JAVA_LANG_ITERABLE, PsiType.VOID, "forEach", new PsiType[1])
&& isVoidContext(terminalCall.getParent())) {
PsiExpression qualifier = terminalCall.getMethodExpression().getQualifierExpression();
if (qualifier == null) return null;
// Do not visit this path if some class implements both Iterable and Stream
PsiType type = qualifier.getType();
if (InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) return null;
PsiExpression[] args = terminalCall.getArgumentList().getExpressions();
if (args.length != 1) return null;
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
if (fn == null) return null;
PsiType elementType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_LANG_ITERABLE, 0, false);
if(!isValidElementType(elementType, terminalCall, true)) return null;
elementType = GenericsUtil.getVariableTypeByExpressionType(elementType);
TerminalOperation terminal = new TerminalOperation.ForEachTerminalOperation(fn);
SourceOperation source = new SourceOperation.ForEachSource(qualifier);
OperationRecord terminalRecord = new OperationRecord();
OperationRecord sourceRecord = new OperationRecord();
terminalRecord.myOperation = terminal;
sourceRecord.myOperation = source;
sourceRecord.myOutVar = terminalRecord.myInVar = new StreamVariable(elementType);
sourceRecord.myInVar = terminalRecord.myOutVar = StreamVariable.STUB;
return Arrays.asList(sourceRecord, terminalRecord);
}
return null;
if (!ITERABLE_FOREACH.test(terminalCall) || !isVoidContext(terminalCall.getParent())) return null;
PsiExpression qualifier = terminalCall.getMethodExpression().getQualifierExpression();
if (qualifier == null) return null;
// Do not visit this path if some class implements both Iterable and Stream
PsiType type = qualifier.getType();
if (InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) return null;
PsiExpression arg = terminalCall.getArgumentList().getExpressions()[0];
FunctionHelper fn = FunctionHelper.create(arg, 1, true);
if (fn == null) return null;
PsiType elementType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_LANG_ITERABLE, 0, false);
if (!isValidElementType(elementType, terminalCall, true)) return null;
elementType = GenericsUtil.getVariableTypeByExpressionType(elementType);
TerminalOperation terminal = new TerminalOperation.ForEachTerminalOperation(fn);
SourceOperation source = new SourceOperation.ForEachSource(qualifier);
OperationRecord terminalRecord = new OperationRecord();
OperationRecord sourceRecord = new OperationRecord();
terminalRecord.myOperation = terminal;
sourceRecord.myOperation = source;
sourceRecord.myOutVar = terminalRecord.myInVar = new StreamVariable(elementType);
sourceRecord.myInVar = terminalRecord.myOutVar = StreamVariable.STUB;
return Arrays.asList(sourceRecord, terminalRecord);
}
@Nullable
static List<OperationRecord> extractMapForEach(PsiMethodCallExpression terminalCall) {
if (!MAP_FOREACH.test(terminalCall) || !isVoidContext(terminalCall.getParent())) return null;
PsiExpression qualifier = terminalCall.getMethodExpression().getQualifierExpression();
if (qualifier == null) return null;
// Do not visit this path if some class implements both Map and Stream
PsiType type = qualifier.getType();
if (InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) return null;
PsiExpression arg = terminalCall.getArgumentList().getExpressions()[0];
FunctionHelper fn = FunctionHelper.create(arg, 2, true);
if (fn == null) return null;
PsiType keyType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_UTIL_MAP, 0, false);
PsiType valueType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_UTIL_MAP, 1, false);
if (!isValidElementType(keyType, terminalCall, true) || !isValidElementType(valueType, terminalCall, true)) return null;
keyType = GenericsUtil.getVariableTypeByExpressionType(keyType);
valueType = GenericsUtil.getVariableTypeByExpressionType(valueType);
Project project = terminalCall.getProject();
JavaPsiFacade facade = JavaPsiFacade.getInstance(project);
PsiClass entryClass = facade.findClass(CommonClassNames.JAVA_UTIL_MAP_ENTRY, terminalCall.getResolveScope());
if (entryClass == null || entryClass.getTypeParameters().length != 2) return null;
PsiType entryType = JavaPsiFacade.getElementFactory(project).createType(entryClass, keyType, valueType);
TerminalOperation terminal = new TerminalOperation.MapForEachTerminalOperation(fn, keyType, valueType);
SourceOperation source = new SourceOperation.ForEachSource(qualifier, true);
OperationRecord terminalRecord = new OperationRecord();
OperationRecord sourceRecord = new OperationRecord();
terminalRecord.myOperation = terminal;
sourceRecord.myOperation = source;
sourceRecord.myOutVar = terminalRecord.myInVar = new StreamVariable(entryType);
sourceRecord.myInVar = terminalRecord.myOutVar = StreamVariable.STUB;
return Arrays.asList(sourceRecord, terminalRecord);
}
@Nullable
@@ -263,6 +297,9 @@ public class StreamToLoopInspection extends AbstractBaseJavaLocalInspectionTool
if (operations == null) {
operations = extractIterableForEach(terminalCall);
}
if (operations == null) {
operations = extractMapForEach(terminalCall);
}
TerminalOperation terminal = getTerminal(operations);
if (terminal == null) return;
PsiStatement statement = ObjectUtils.tryCast(RefactoringUtil.getParentStatement(terminalCall, false), PsiStatement.class);
@@ -1169,6 +1169,48 @@ abstract class TerminalOperation extends Operation {
}
}
static class MapForEachTerminalOperation extends TerminalOperation {
private final FunctionHelper myFn;
private final PsiType myKeyType;
private final PsiType myValueType;
public MapForEachTerminalOperation(FunctionHelper fn, PsiType keyType, PsiType valueType) {
myFn = fn;
myKeyType = keyType;
myValueType = valueType;
}
@Override
public void preprocessVariables(StreamToLoopReplacementContext context, StreamVariable inVar, StreamVariable outVar) {
inVar.addBestNameCandidate("entry");
inVar.addBestNameCandidate("e");
inVar.addBestNameCandidate("mapEntry");
}
@Override
public void registerReusedElements(Consumer<PsiElement> consumer) {
myFn.registerReusedElements(consumer);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
StreamVariable keyVar = new StreamVariable(myKeyType);
myFn.preprocessVariable(context, keyVar, 0);
keyVar.addBestNameCandidate("key");
keyVar.addBestNameCandidate("k");
StreamVariable valueVar = new StreamVariable(myValueType);
myFn.preprocessVariable(context, valueVar, 1);
valueVar.addBestNameCandidate("value");
valueVar.addBestNameCandidate("v");
keyVar.register(context);
valueVar.register(context);
myFn.transform(context, keyVar.getName(), valueVar.getName());
return keyVar.getDeclaration(inVar.getName() + ".getKey()") +
valueVar.getDeclaration(inVar.getName() + ".getValue()") +
myFn.getStatementText();
}
}
static class SortedTerminalOperation extends TerminalOperation {
private final AccumulatedOperation myOrigin;
@Nullable private final PsiExpression myComparator;
@@ -1,6 +1,6 @@
<html>
<body>
Finds Stream API chains and provides quick fix to convert them into classical loops.
Finds Stream API chains, <b>Iterable.forEach</b> and <b>Map.forEach</b> calls and provides quick fix to convert them into classical loops.
<p>
Note that sometimes this inspection might cause slight semantic changes.
Special care should be taken when it comes to short-circuiting, as it's not specified how many elements will be actually read when
@@ -0,0 +1,34 @@
// "Fix all 'Stream API call chain can be replaced with loop' problems in file" "true"
import java.util.Map;
import java.util.function.BiConsumer;
public class Main {
void test(Map<String, Integer> map) {
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String k = entry.getKey();
Integer v = entry.getValue();
if (k.isEmpty()) continue;
System.out.println("Key: " + k + "; value: " + v);
}
}
void test(Map<String, Integer> map, BiConsumer<String, Integer> consumer) {
int entry = 1;
for (Map.Entry<String, Integer> e : map.entrySet()) {
String key = e.getKey();
Integer value = e.getValue();
consumer.accept(key, value);
}
}
void test(Map<String, Integer> map, Map<String, Integer> otherMap) {
int entry = 1, e = 2, key = 3, value = 4;
for (Map.Entry<String, Integer> mapEntry : map.entrySet()) {
String k = mapEntry.getKey();
Integer v = mapEntry.getValue();
otherMap.putIfAbsent(k, v);
}
}
}
@@ -0,0 +1,24 @@
// "Fix all 'Stream API call chain can be replaced with loop' problems in file" "true"
import java.util.Map;
import java.util.function.BiConsumer;
public class Main {
void test(Map<String, Integer> map) {
map.fo<caret>rEach((k, v) -> {
if (k.isEmpty()) return;
System.out.println("Key: " + k + "; value: " + v);
});
}
void test(Map<String, Integer> map, BiConsumer<String, Integer> consumer) {
int entry = 1;
map.forEach(consumer);
}
void test(Map<String, Integer> map, Map<String, Integer> otherMap) {
int entry = 1, e = 2, key = 3, value = 4;
map.forEach(otherMap::putIfAbsent);
}
}