mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
StreamToLoopInspection: support Map.forEach()
Fixes IDEA-190440 Convert usage of java.util.Map.forEach() to for (... entrySet()) loop
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+63
-26
@@ -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
|
||||
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user