diff --git a/java/java-impl/src/com/intellij/codeInspection/streamToLoop/SourceOperation.java b/java/java-impl/src/com/intellij/codeInspection/streamToLoop/SourceOperation.java index 4ad1cfac6fe1..de6c4b308c2a 100644 --- a/java/java-impl/src/com/intellij/codeInspection/streamToLoop/SourceOperation.java +++ b/java/java-impl/src/com/intellij/codeInspection/streamToLoop/SourceOperation.java @@ -56,7 +56,7 @@ abstract class SourceOperation extends Operation { static SourceOperation createSource(PsiMethodCallExpression call, boolean supportUnknownSources) { PsiExpression[] args = call.getArgumentList().getExpressions(); PsiType callType = call.getType(); - if(callType == null) return null; + if(callType == null || PsiType.VOID.equals(callType)) return null; PsiMethod method = call.resolveMethod(); if(method == null) return null; String name = method.getName(); diff --git a/java/java-impl/src/com/intellij/codeInspection/streamToLoop/StreamToLoopInspection.java b/java/java-impl/src/com/intellij/codeInspection/streamToLoop/StreamToLoopInspection.java index d6bae18750fe..04ad7b9d3cdd 100644 --- a/java/java-impl/src/com/intellij/codeInspection/streamToLoop/StreamToLoopInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/streamToLoop/StreamToLoopInspection.java @@ -84,25 +84,25 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool { PsiMethod method = call.resolveMethod(); if(method == null) return; PsiClass aClass = method.getContainingClass(); - if(!InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) return; - PsiMethodCallExpression currentCall = call; - while(true) { - Operation op = createOperationFromCall(StreamVariable.STUB, currentCall, SUPPORT_UNKNOWN_SOURCES); - if(op == null) return; - if(op instanceof SourceOperation) { - TextRange range; - if(isOnTheFly && InspectionProjectProfileManager.isInformationLevel(getShortName(), call)) { - range = new TextRange(0, call.getTextLength()); - } else { - range = nameElement.getTextRange().shiftRight(-call.getTextOffset()); - } - holder.registerProblem(call, range, "Replace stream API chain with loop", new ReplaceStreamWithLoopFix()); - return; + if (InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) { + if (extractOperations(StreamVariable.STUB, call, SUPPORT_UNKNOWN_SOURCES) != null) { + register(call, nameElement, "Replace Stream API chain with loop"); } - PsiExpression qualifier = currentCall.getMethodExpression().getQualifierExpression(); - if(!(qualifier instanceof PsiMethodCallExpression)) return; - currentCall = (PsiMethodCallExpression)qualifier; } + else if (extractIterableForEach(call) != null) { + register(call, nameElement, "Replace 'forEach' call with loop"); + } + } + + private void register(PsiMethodCallExpression call, PsiElement nameElement, String message) { + TextRange range; + if (isOnTheFly && InspectionProjectProfileManager.isInformationLevel(getShortName(), call)) { + range = new TextRange(0, call.getTextLength()); + } + else { + range = nameElement.getTextRange().shiftRight(-call.getTextOffset()); + } + holder.registerProblem(call, range, message, new ReplaceStreamWithLoopFix(message)); } }; } @@ -169,11 +169,7 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool { } Operation op = Operation.createIntermediate(name, args, outVar, elementType, supportUnknownSources); if (op != null) return op; - PsiElement parent = call.getParent(); - boolean isVoid = parent instanceof PsiExpressionStatement || - (parent instanceof PsiLambdaExpression && - PsiType.VOID.equals(LambdaUtil.getFunctionalInterfaceReturnType((PsiLambdaExpression)parent))); - op = TerminalOperation.createTerminal(name, args, elementType, callType, isVoid); + op = TerminalOperation.createTerminal(name, args, elementType, callType, isVoidContext(call.getParent())); if (op != null) return op; } return null; @@ -181,6 +177,39 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool { return SourceOperation.createSource(call, supportUnknownSources); } + private static boolean isVoidContext(PsiElement element) { + return element instanceof PsiExpressionStatement || + (element instanceof PsiLambdaExpression && + PsiType.VOID.equals(LambdaUtil.getFunctionalInterfaceReturnType((PsiLambdaExpression)element))); + } + + @Nullable + static List 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); + if (fn == null) return null; + PsiType elementType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_LANG_ITERABLE, 0, false); + if(elementType == null) return null; + 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.getCanonicalText()); + sourceRecord.myInVar = terminalRecord.myOutVar = StreamVariable.STUB; + return Arrays.asList(sourceRecord, terminalRecord); + } + return null; + } @Nullable static List extractOperations(StreamVariable outVar, @@ -234,6 +263,19 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool { } static class ReplaceStreamWithLoopFix implements LocalQuickFix { + private String myMessage; + + public ReplaceStreamWithLoopFix(String message) { + myMessage = message; + } + + @Nls + @NotNull + @Override + public String getName() { + return myMessage; + } + @Nls @NotNull @Override @@ -253,6 +295,9 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool { PsiType resultType = terminalCall.getType(); if (resultType == null) return; List operations = extractOperations(StreamVariable.STUB, terminalCall, true); + if (operations == null) { + operations = extractIterableForEach(terminalCall); + } TerminalOperation terminal = getTerminal(operations); if (terminal == null) return; allOperations(operations).forEach(or -> or.myOperation.suggestNames(or.myInVar, or.myOutVar)); diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamToLoop/afterIterableForEach.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamToLoop/afterIterableForEach.java new file mode 100644 index 000000000000..fcdb657d581f --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamToLoop/afterIterableForEach.java @@ -0,0 +1,22 @@ +// "Fix all 'Stream API call chain can be replaced with loop' problems in file" "true" + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +public class Main { + public void testCast(Object obj, List list) { + for (Number n : ((Iterable) obj)) { + list.add(n); + } + } + + public static void main(String[] args) { + List list = Arrays.asList("a", "b"); + for (String s : list) { + System.out.println(s); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamToLoop/beforeIterableForEach.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamToLoop/beforeIterableForEach.java new file mode 100644 index 000000000000..032bc04e31b5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/streamToLoop/beforeIterableForEach.java @@ -0,0 +1,18 @@ +// "Fix all 'Stream API call chain can be replaced with loop' problems in file" "true" + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; + +public class Main { + public void testCast(Object obj, List list) { + ((Iterable)obj).forEach(n -> list.add(n)); + } + + public static void main(String[] args) { + List list = Arrays.asList("a", "b"); + list.forEach(System.out::println); + } +} \ No newline at end of file